Signal-iOS/SignalServiceKit/src/Messages/MessageSender.m
Michelle Linington 436acf2694 Various bug fixes and improvements
Most significant here is better handling of quoted reply content without
an attachment. Or with an attachment without a thumbnail
2021-08-26 22:32:30 -07:00

1586 lines
68 KiB
Objective-C

//
// Copyright (c) 2021 Open Whisper Systems. All rights reserved.
//
#import "MessageSender.h"
#import "NSData+keyVersionByte.h"
#import "NSData+messagePadding.h"
#import "PreKeyBundle+jsonDict.h"
#import <AFNetworking/AFURLResponseSerialization.h>
#import <PromiseKit/AnyPromise.h>
#import <SignalCoreKit/NSData+OWS.h>
#import <SignalCoreKit/NSDate+OWS.h>
#import <SignalCoreKit/SCKExceptionWrapper.h>
#import <SignalCoreKit/Threading.h>
#import <SignalMetadataKit/SignalMetadataKit-Swift.h>
#import <SignalServiceKit/AppContext.h>
#import <SignalServiceKit/AxolotlExceptions.h>
#import <SignalServiceKit/FunctionalUtil.h>
#import <SignalServiceKit/HTTPUtils.h>
#import <SignalServiceKit/OWSBackgroundTask.h>
#import <SignalServiceKit/OWSContact.h>
#import <SignalServiceKit/OWSDevice.h>
#import <SignalServiceKit/OWSDisappearingMessagesJob.h>
#import <SignalServiceKit/OWSDispatch.h>
#import <SignalServiceKit/OWSError.h>
#import <SignalServiceKit/OWSIdentityManager.h>
#import <SignalServiceKit/OWSOperation.h>
#import <SignalServiceKit/OWSOutgoingCallMessage.h>
#import <SignalServiceKit/OWSOutgoingGroupCallMessage.h>
#import <SignalServiceKit/OWSOutgoingReactionMessage.h>
#import <SignalServiceKit/OWSOutgoingSentMessageTranscript.h>
#import <SignalServiceKit/OWSOutgoingSyncMessage.h>
#import <SignalServiceKit/OWSUploadOperation.h>
#import <SignalServiceKit/ProfileManagerProtocol.h>
#import <SignalServiceKit/SSKEnvironment.h>
#import <SignalServiceKit/SSKPreKeyStore.h>
#import <SignalServiceKit/SSKSignedPreKeyStore.h>
#import <SignalServiceKit/SignalRecipient.h>
#import <SignalServiceKit/SignalServiceKit-Swift.h>
#import <SignalServiceKit/TSAccountManager.h>
#import <SignalServiceKit/TSAttachmentStream.h>
#import <SignalServiceKit/TSContactThread.h>
#import <SignalServiceKit/TSGroupThread.h>
#import <SignalServiceKit/TSIncomingMessage.h>
#import <SignalServiceKit/TSInfoMessage.h>
#import <SignalServiceKit/TSOutgoingMessage.h>
#import <SignalServiceKit/TSPreKeyManager.h>
#import <SignalServiceKit/TSQuotedMessage.h>
#import <SignalServiceKit/TSRequest.h>
#import <SignalServiceKit/TSThread.h>
NS_ASSUME_NONNULL_BEGIN
NSString *NoSessionForTransientMessageException = @"NoSessionForTransientMessageException";
const NSUInteger kOversizeTextMessageSizeThreshold = 2 * 1024;
NSError *SSKEnsureError(NSError *_Nullable error, OWSErrorCode fallbackCode, NSString *fallbackErrorDescription)
{
if (error) {
return error;
}
OWSCFailDebug(@"Using fallback error.");
// TODO: Audit all of the places this is called and replace with more specific errors.
return [OWSError withError:fallbackCode description:fallbackErrorDescription isRetryable:true];
}
#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
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) void (^successHandler)(void);
@property (nonatomic, readonly) void (^failureHandler)(NSError *error);
@end
#pragma mark -
@implementation OWSSendMessageOperation
- (instancetype)initWithMessage:(TSOutgoingMessage *)message
messageSender:(MessageSender *)messageSender
success:(void (^)(void))successHandler
failure:(void (^)(NSError *error))failureHandler
{
self = [super init];
if (!self) {
return self;
}
_message = message;
_messageSender = messageSender;
_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 (IsNetworkConnectivityFailure(error)) {
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;
}
if ((self.message.shouldBeSaved && latestCopy == nil) || messageWasRemotelyDeleted) {
OWSLogInfo(@"aborting message send; message deleted.");
NSError *error = [MessageDeletedBeforeSentError asNSError];
[self reportError:error];
return;
}
[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();
}
- (void)didFailWithError:(NSError *)error
{
OWSLogError(@"Failed with error: %@ (isRetryable: %d)", error, error.isRetryable);
self.failureHandler(error);
}
@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
@interface MessageSender (ImplementedInSwift)
- (nullable NSDictionary *)encryptedMessageForMessageSend:(OWSMessageSend *)messageSend
deviceId:(int)deviceId
transaction:(SDSAnyWriteTransaction *)transaction
error:(NSError **)error;
@end
#pragma mark -
@implementation MessageSender
- (instancetype)init
{
self = [super init];
if (!self) {
return self;
}
_sendingQueueMap = [NSMutableDictionary new];
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 = [NSString stringWithFormat:@"%@:%@", self.logTag, queueKey];
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 = (SSKFeatureFlags.attachmentUploadV3ForV1GroupAvatars
|| message.groupMetaMessage == TSGroupMetaMessageUnspecified
|| message.groupMetaMessage == TSGroupMetaMessageDeliver);
OWSSendMessageOperation *sendMessageOperation =
[[OWSSendMessageOperation alloc] initWithMessage:message
messageSender:self
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 promiseWithResolverBlock:^(PMKResolver resolve) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
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
rotateSignedPreKeyWithSuccess:^{
OWSLogInfo(@"New prekeys registered with server.");
resolve(@(1));
}
failure:^(NSError *error) {
OWSLogWarn(@"Failed to update prekeys with the server: %@", error);
resolve(error);
}];
});
}];
}
- (AnyPromise *)sendPromiseForAddresses:(NSArray<SignalServiceAddress *> *)addresses
message:(TSOutgoingMessage *)message
thread:(TSThread *)thread
senderCertificates:(nullable SenderCertificates *)senderCertificates
sendErrorBlock:(void (^_Nonnull)(SignalServiceAddress *address, NSError *))sendErrorBlock
{
OWSAssertDebug(!NSThread.isMainThread);
OWSAssertDebug(addresses.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" using a single write transaction.
NSMutableDictionary<SignalServiceAddress *, OWSUDSendingAccess *> *sendingAccessMap = [NSMutableDictionary new];
if (senderCertificates != nil) {
for (SignalServiceAddress *address in addresses) {
if (!address.isLocalAddress) {
sendingAccessMap[address] = [self.udManager udSendingAccessForAddress:address
requireSyncAccess:YES
senderCertificates:senderCertificates];
}
}
}
// 3. If we have any participants that support sender key, build a promise for their send.
NSArray<SignalServiceAddress *> *senderKeyAddresses = [self senderKeyParticipantsWithThread:thread
intendedRecipients:addresses
udAccessMap:sendingAccessMap];
AnyPromise *_Nullable senderKeyMessagePromise = nil;
NSArray<SignalServiceAddress *> *fanoutSendAddresses = addresses;
if ([thread isKindOfClass:[TSGroupThread class]] && senderKeyAddresses.count >= 2 && message.canSendWithSenderKey) {
TSGroupThread *groupThread = (TSGroupThread *)thread;
fanoutSendAddresses = [addresses
filter:^BOOL(SignalServiceAddress *_Nonnull item) { return ![senderKeyAddresses containsObject:item]; }];
senderKeyMessagePromise = [self senderKeyMessageSendPromiseWithMessage:message
plaintextContent:plaintext
payloadId:plaintextPayloadId
thread:groupThread
recipients:senderKeyAddresses
udAccessMap:sendingAccessMap
senderCertificates:senderCertificates
sendErrorBlock:sendErrorBlock];
OWSLogDebug(@"%lu / %lu recipients for message: %llu support sender key.",
senderKeyAddresses.count,
addresses.count,
message.timestamp);
} else {
senderKeyAddresses = @[];
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((fanoutSendAddresses.count + senderKeyAddresses.count) == addresses.count);
// 4. Build a "OWSMessageSend" for each non-senderKey recipient.
NSMutableArray<OWSMessageSend *> *messageSends = [NSMutableArray new];
for (SignalServiceAddress *address in fanoutSendAddresses) {
OWSUDSendingAccess *_Nullable udSendingAccess = sendingAccessMap[address];
OWSMessageSend *messageSend =
[[OWSMessageSend alloc] initWithMessage:message
plaintextContent:plaintext
plaintextPayloadId:plaintextPayloadId
thread:thread
address:address
udSendingAccess:udSendingAccess
localAddress:self.tsAccountManager.localAddress
sendErrorBlock:^(NSError *error) { sendErrorBlock(address, 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 PMKJoin(), not PMKWhen(), because we don't want the
// completion promise to execute until _all_ send promises
// have either succeeded or failed. PMKWhen() executes as
// soon as any of its input promises fail.
return PMKJoin(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<SignalServiceAddress *> *recipientAddresses = sendInfo.recipients;
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 a sync message with a delivery receipt.
if (contactThread.contactAddress.isLocalAddress && !message.isSyncMessage && !message.isCallMessage) {
// Send to self.
OWSAssertDebug(sendInfo.recipients.count == 1);
// Don't mark self-sent messages as read (or sent) until the sync transcript is sent.
successHandler();
return;
}
}
if (recipientAddresses.count < 1) {
// All recipients are already sent or can be skipped.
// NOTE: We might still need to send a sync transcript.
successHandler();
return;
}
BOOL isGroupSend = thread.isGroupThread;
NSMutableArray<NSError *> *sendErrors = [NSMutableArray array];
NSMutableDictionary<SignalServiceAddress *, NSError *> *sendErrorPerRecipient = [NSMutableDictionary dictionary];
[self unlockPreKeyUpdateFailuresPromise]
.thenInBackground(^(id value) {
return [self sendPromiseForAddresses:recipientAddresses
message:message
thread:thread
senderCertificates:senderCertificates
sendErrorBlock:^(SignalServiceAddress *address, NSError *error) {
@synchronized(sendErrors) {
[sendErrors addObject:error];
sendErrorPerRecipient[address] = error;
}
}];
})
.thenInBackground(^(id value) { successHandler(); })
.catchInBackground(^(id failure) {
NSError *firstRetryableError = nil;
NSError *firstNonRetryableError = nil;
NSArray<NSError *> *sendErrorsCopy;
NSDictionary<SignalServiceAddress *, NSError *> *sendErrorPerRecipientCopy;
@synchronized(sendErrors) {
sendErrorsCopy = [sendErrors copy];
sendErrorPerRecipientCopy = [sendErrorPerRecipient copy];
}
// Record the individual error for each "failed" recipient.
DatabaseStorageWrite(self.databaseStorage, ^(SDSAnyWriteTransaction *transaction) {
for (SignalServiceAddress *address in sendErrorPerRecipientCopy) {
NSError *error = sendErrorPerRecipientCopy[address];
// Some errors should be ignored when sending messages
// to groups. See discussion on
// NSError (MessageSender) category.
if (isGroupSend && [error shouldBeIgnoredForGroups]) {
continue;
}
// We only want to record a failure for errors we can
// no longer retry.
if (error.isRetryable) {
continue;
}
[message updateWithFailedRecipient:address error:error transaction:transaction];
}
});
for (NSError *error in sendErrorsCopy) {
// Some errors should be ignored when sending messages
// to groups. See discussion on
// NSError (MessageSender) category.
if (isGroupSend && [error shouldBeIgnoredForGroups]) {
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.
BOOL isSyncMessage = [message isKindOfClass:[OWSOutgoingSyncMessage class]];
if (thread == nil && 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<NSDictionary *> *)deviceMessagesForMessageSend:(OWSMessageSend *)messageSend
error:(NSError **)errorHandle
{
OWSAssertDebug(!NSThread.isMainThread);
OWSAssertDebug(messageSend);
OWSAssertDebug(errorHandle);
SignalServiceAddress *address = messageSend.address;
NSArray<NSDictionary *> *deviceMessages;
@try {
deviceMessages = [self throws_deviceMessagesForMessageSend:messageSend];
} @catch (NSException *exception) {
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.
NSError *error = MessageSenderNoSessionForTransientMessageError.asNSError;
*errorHandle = error;
return nil;
} 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]);
NSError *error = [UntrustedIdentityError asNSErrorWithAddress:address];
*errorHandle = error;
return nil;
}
if ([exception.name isEqualToString:MessageSenderRateLimitedException]) {
NSError *error = [SignalServiceRateLimitedError asNSError];
*errorHandle = error;
return nil;
}
if ([exception.name isEqualToString:MessageSenderSpamChallengeResolvedException]) {
NSError *error = [SpamChallengeResolvedError asNSError];
*errorHandle = error;
return nil;
}
if ([exception.name isEqualToString:MessageSenderSpamChallengeRequiredException]) {
NSError *error = [SpamChallengeRequiredError asNSError];
*errorHandle = error;
return nil;
}
OWSLogWarn(@"Could not build device messages: %@", exception);
NSError *error = [OWSRetryableMessageSenderError asNSError];
*errorHandle = error;
return nil;
}
return deviceMessages;
}
- (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.sendMessageToRecipient";
operationQueue.maxConcurrentOperationCount = 6;
});
[operationQueue addOperationWithBlock:^{ [self _sendMessageToRecipient:messageSend]; }];
}
- (void)_sendMessageToRecipient:(OWSMessageSend *)messageSend
{
OWSAssertDebug(!NSThread.isMainThread);
OWSAssertDebug(messageSend);
OWSAssertDebug(messageSend.thread);
TSOutgoingMessage *message = messageSend.message;
SignalServiceAddress *address = messageSend.address;
OWSAssertDebug(address.isValid);
OWSLogInfo(
@"attempting to send message: %@, timestamp: %llu, recipient: %@", message.class, message.timestamp, address);
if (messageSend.remainingAttempts <= 0) {
// We should always fail with a specific error.
OWSProdFail([OWSAnalyticsEvents messageSenderErrorGenericSendFailure]);
NSError *error = [OWSRetryableMessageSenderError asNSError];
return messageSend.failure(error);
}
// A prior CDS lookup would've resolved the UUID for this recipient if it was registered
// If we have no UUID, consider the recipient unregistered.
BOOL isInvalidRecipient = (address.uuid == nil);
if (isInvalidRecipient) {
[self failSendForUnregisteredRecipient:messageSend];
return;
}
// 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 isKindOfClass:[OWSOutgoingSyncMessage class]]
&& ![message isKindOfClass:[OWSOutgoingSentMessageTranscript class]]) {
[messageSend disableUD];
} else if (SSKDebugFlags.disableUD.value) {
OWSLogDebug(@"Disabling UD because of testable flag");
[messageSend disableUD];
}
NSError *deviceMessagesError;
NSArray<NSDictionary *> *_Nullable deviceMessages = [self deviceMessagesForMessageSend:messageSend
error:&deviceMessagesError];
if (deviceMessagesError || !deviceMessages) {
OWSAssertDebug(deviceMessagesError);
return messageSend.failure(deviceMessagesError);
}
if (messageSend.isLocalAddress) {
OWSAssertDebug(message.isSyncMessage || message.isCallMessage);
// Messages sent to the "local number" should be sync messages or call messages.
//
// 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 (NSDictionary<NSString *, id> *deviceMessage in deviceMessages) {
NSString *_Nullable destination = deviceMessage[@"destination"];
if (!destination) {
OWSFailDebug(@"Sync device message missing destination: %@", deviceMessage);
continue;
}
SignalServiceAddress *destinationAddress;
if ([[NSUUID alloc] initWithUUIDString:destination]) {
destinationAddress = [[SignalServiceAddress alloc] initWithUuidString:destination];
} else {
destinationAddress = [[SignalServiceAddress alloc] initWithPhoneNumber:destination];
}
NSNumber *_Nullable destinationDeviceId = deviceMessage[@"destinationDeviceId"];
if (!destinationDeviceId) {
OWSFailDebug(@"Sync device message missing destination device id: %@", deviceMessage);
continue;
}
if (destinationDeviceId.intValue != 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 (NSDictionary *deviceMessage in deviceMessages) {
TSWhisperMessageType messageType = [deviceMessage[@"type"] integerValue];
BOOL hasValidMessageType = NO;
if (messageSend.isUDSend) {
hasValidMessageType |= (messageType == TSUnidentifiedSenderMessageType);
} else {
hasValidMessageType |= (messageType == TSEncryptedWhisperMessageType);
hasValidMessageType |= (messageType == TSPreKeyWhisperMessageType);
hasValidMessageType |= (messageType == TSPlaintextMessageType);
}
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;
}
BOOL isSyncMessage = [message isKindOfClass:[OWSOutgoingSyncMessage class]];
if (contactThread && contactThread.contactAddress.isLocalAddress && !isSyncMessage) {
OWSAssertDebug(message.recipientAddresses.count == 1);
// Don't mark self-sent messages as read (or sent) until the sync transcript is sent.
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();
}
BOOL shouldSendTranscript = (SSKFeatureFlags.sendRecipientUpdates || !message.hasSyncedTranscript);
if (!shouldSendTranscript) {
return success();
}
BOOL isRecipientUpdate = message.hasSyncedTranscript;
[self sendSyncTranscriptForMessage:message
isRecipientUpdate:isRecipientUpdate
success:^{
DatabaseStorageWrite(self.databaseStorage, ^(SDSAnyWriteTransaction *transaction) {
[message updateWithHasSyncedTranscript:YES transaction:transaction];
});
success();
}
failure:failure];
}
- (void)sendSyncTranscriptForMessage:(TSOutgoingMessage *)message
isRecipientUpdate:(BOOL)isRecipientUpdate
success:(void (^)(void))success
failure:(RetryableFailureHandler)failure
{
OWSAssertDebug(!NSThread.isMainThread);
SignalServiceAddress *localAddress = self.tsAccountManager.localAddress;
// After sending a message to its "message thread",
// we send a sync transcript to the "local thread".
__block TSThread *_Nullable localThread;
__block TSThread *_Nullable messageThread;
__block OWSOutgoingSentMessageTranscript *sentMessageTranscript;
__block NSData *_Nullable plaintext;
__block NSNumber *_Nullable plaintextPayloadId;
DatabaseStorageWrite(self.databaseStorage, ^(SDSAnyWriteTransaction *transaction) {
localThread = [TSAccountManager getOrCreateLocalThreadWithTransaction:transaction];
messageThread = [self threadForMessage:message transaction:transaction];
sentMessageTranscript = [[OWSOutgoingSentMessageTranscript alloc] initWithLocalThread:localThread
messageThread:messageThread
outgoingMessage:message
isRecipientUpdate:isRecipientUpdate];
plaintext = [sentMessageTranscript buildPlainTextData:localThread transaction:transaction];
plaintextPayloadId = [MessageSendLog recordPayload:plaintext
forMessageBeingSent:sentMessageTranscript
transaction:transaction];
});
if (localThread == nil) {
return failure(OWSErrorMakeAssertionError(@"Missing local thread"));
}
if (messageThread == nil) {
return failure(OWSErrorMakeAssertionError(@"Missing message thread"));
}
if (plaintext == nil) {
return failure(OWSErrorMakeAssertionError(@"Missing proto"));
}
OWSMessageSend *messageSend = [[OWSMessageSend alloc] initWithMessage:sentMessageTranscript
plaintextContent:plaintext
plaintextPayloadId:plaintextPayloadId
thread:localThread
address:localAddress
udSendingAccess:nil
localAddress:localAddress
sendErrorBlock:nil];
[MessageSender ensureSessionsforMessageSendsObjc:@[ messageSend ] ignoreErrors:YES]
.thenInBackground(^(id value) {
[self sendMessageToRecipient:messageSend];
return messageSend.asAnyPromise;
})
.thenInBackground(^{
OWSLogInfo(@"Successfully sent sync transcript.");
success();
})
.catchInBackground(^(NSError *error) {
OWSLogInfo(@"Failed to send sync transcript: %@ (isRetryable: %d)", error, [error isRetryable]);
failure(error);
});
}
- (NSArray<NSDictionary *> *)throws_deviceMessagesForMessageSend:(OWSMessageSend *)messageSend
{
OWSAssertDebug(!NSThread.isMainThread);
OWSAssertDebug(messageSend.message);
OWSAssertDebug(messageSend.address.isValid);
__block SignalRecipient *recipient;
[self.databaseStorage readWithBlock:^(SDSAnyReadTransaction *transaction) {
recipient = [SignalRecipient getRecipientForAddress:messageSend.address
mustHaveDevices:NO
transaction:transaction];
}];
if (!recipient) {
OWSRaiseException(InvalidMessageException, @"Unexpectedly missing recipient");
}
if (!messageSend.plaintextContent) {
OWSRaiseException(InvalidMessageException, @"No message proto");
}
NSMutableArray<NSNumber *> *deviceIds = [recipient.devices.array mutableCopy];
OWSAssertDebug(deviceIds);
NSMutableArray *messagesArray = [NSMutableArray arrayWithCapacity:deviceIds.count];
OWSLogVerbose(@"building device messages for: %@ %@ (isLocalAddress: %d, isUDSend: %d)",
recipient.address,
deviceIds,
messageSend.isLocalAddress,
messageSend.isUDSend);
if (messageSend.isLocalAddress) {
[deviceIds removeObject:@(self.tsAccountManager.storedDeviceId)];
}
for (NSNumber *deviceId in deviceIds) {
@try {
// This may involve blocking network requests.
[self throws_ensureRecipientHasSessionForMessageSend:messageSend
deviceId:deviceId
accountId:recipient.accountId];
} @catch (NSException *exception) {
if ([exception.name isEqualToString:MessageSenderInvalidDeviceException]) {
DatabaseStorageWrite(self.databaseStorage, ^(SDSAnyWriteTransaction *transaction) {
[MessageSender updateDevicesWithAddress:messageSend.address
devicesToAdd:@[]
devicesToRemove:@[ deviceId ]
transaction:transaction];
});
[deviceIds removeObject:deviceId];
} else {
@throw exception;
}
}
}
__block NSError *encryptionError;
DatabaseStorageWrite(self.databaseStorage, ^(SDSAnyWriteTransaction *transaction) {
for (NSNumber *deviceId in deviceIds) {
NSDictionary *_Nullable messageDict = [self encryptedMessageForMessageSend:messageSend
deviceId:deviceId.intValue
transaction:transaction
error:&encryptionError];
if (!messageDict) {
return;
}
[messagesArray addObject:messageDict];
}
});
if (encryptionError) {
OWSRaiseException(InvalidMessageException, @"Failed to encrypt message: %@", encryptionError);
}
return [messagesArray copy];
}
- (void)throws_ensureRecipientHasSessionForMessageSend:(OWSMessageSend *)messageSend
deviceId:(NSNumber *)deviceId
accountId:(NSString *)accountId
{
OWSAssertDebug(!NSThread.isMainThread);
OWSAssertDebug(messageSend);
OWSAssertDebug(deviceId);
OWSAssertDebug(accountId);
SignalServiceAddress *recipientAddress = messageSend.address;
OWSAssertDebug(recipientAddress.isValid);
__block BOOL hasSession;
[self.databaseStorage readWithBlock:^(SDSAnyReadTransaction *transaction) {
hasSession = [self.sessionStore containsActiveSessionForAccountId:accountId
deviceId:[deviceId intValue]
transaction:transaction];
}];
if (hasSession) {
return;
}
// Discard "typing indicator" messages if there is no existing session with the user.
BOOL canSafelyBeDiscarded = messageSend.message.isOnline;
if (canSafelyBeDiscarded) {
OWSRaiseException(NoSessionForTransientMessageException, @"No session for transient message.");
}
__block dispatch_semaphore_t sema = dispatch_semaphore_create(0);
__block PreKeyBundle *_Nullable bundle;
__block NSException *_Nullable exception;
[MessageSender makePrekeyRequestWithMessageSend:messageSend
deviceId:deviceId
accountId:accountId
success:^(PreKeyBundle *_Nullable responseBundle) {
bundle = responseBundle;
dispatch_semaphore_signal(sema);
}
failure:^(NSError *error) {
NSNumber *_Nullable statusCode = HTTPStatusCodeForError(error);
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 (IsNetworkConnectivityFailure(error)) {
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] createSessionForPreKeyBundle:bundle
accountId:accountId
recipientAddress:recipientAddress
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 *message) {
TSAttachmentStream *thumbnail = [message.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 *outgoingMessage) {
NSMutableArray<NSString *> *attachmentIds =
[outgoingMessage.attachmentIds mutableCopy];
for (TSAttachmentStream *attachmentStream in attachmentStreams) {
[attachmentIds addObject:attachmentStream.uniqueId];
}
outgoingMessage.attachmentIds = [attachmentIds copy];
}];
for (TSAttachmentStream *attachmentStream in attachmentStreams) {
[attachmentStream anyInsertWithTransaction:transaction];
}
return YES;
}
@end
NS_ASSUME_NONNULL_END