session-ios/SignalServiceKit/src/Messages/OWSMessageSender.m

1761 lines
76 KiB
Objective-C

//
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
//
#import "OWSMessageSender.h"
#import "AppContext.h"
#import "NSData+keyVersionByte.h"
#import "NSData+messagePadding.h"
#import "NSError+MessageSending.h"
#import "OWSBackgroundTask.h"
#import "OWSBlockingManager.h"
#import "OWSContact.h"
#import "OWSDevice.h"
#import "OWSDisappearingMessagesJob.h"
#import "OWSDispatch.h"
#import "OWSEndSessionMessage.h"
#import "OWSError.h"
#import "OWSIdentityManager.h"
#import "OWSMessageServiceParams.h"
#import "OWSOperation.h"
#import "OWSOutgoingSentMessageTranscript.h"
#import "OWSOutgoingSyncMessage.h"
#import "OWSPrimaryStorage+PreKeyStore.h"
#import "OWSPrimaryStorage+SignedPreKeyStore.h"
#import "OWSPrimaryStorage+sessionStore.h"
#import "OWSPrimaryStorage+Loki.h"
#import "OWSPrimaryStorage.h"
#import "OWSRequestFactory.h"
#import "OWSUploadOperation.h"
#import "PreKeyBundle+jsonDict.h"
#import "SSKEnvironment.h"
#import "SignalRecipient.h"
#import "TSAccountManager.h"
#import "TSAttachmentStream.h"
#import "TSContactThread.h"
#import "TSGroupThread.h"
#import "TSIncomingMessage.h"
#import "TSInfoMessage.h"
#import "TSInvalidIdentityKeySendingErrorMessage.h"
#import "TSNetworkManager.h"
#import "TSOutgoingMessage.h"
#import "TSPreKeyManager.h"
#import "TSQuotedMessage.h"
#import "TSRequest.h"
#import "TSSocketManager.h"
#import "TSThread.h"
#import "TSContactThread.h"
#import "LKDeviceLinkMessage.h"
#import "LKUnlinkDeviceMessage.h"
#import <SessionAxolotlKit/AxolotlExceptions.h>
#import <SessionAxolotlKit/CipherMessage.h>
#import <SessionAxolotlKit/PreKeyBundle.h>
#import <SessionAxolotlKit/SessionBuilder.h>
#import <SessionAxolotlKit/SessionCipher.h>
#import <PromiseKit/AnyPromise.h>
#import <SessionCoreKit/NSData+OWS.h>
#import <SessionCoreKit/NSDate+OWS.h>
#import <SessionCoreKit/SCKExceptionWrapper.h>
#import <SessionCoreKit/Threading.h>
#import <SessionMetadataKit/SessionMetadataKit-Swift.h>
#import <SessionServiceKit/SessionServiceKit-Swift.h>
#import <SessionServiceKit/ProfileManagerProtocol.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.");
return OWSErrorWithCodeDescription(fallbackCode, fallbackErrorDescription);
}
#pragma mark -
void AssertIsOnSendingQueue()
{
#ifdef DEBUG
if (@available(iOS 10.0, *)) {
dispatch_assert_queue([OWSDispatch sendingQueue]);
} // else, skip assert as it's a development convenience.
#endif
}
#pragma mark -
@implementation OWSOutgoingAttachmentInfo
- (instancetype)initWithDataSource:(DataSource *)dataSource
contentType:(NSString *)contentType
sourceFilename:(nullable NSString *)sourceFilename
caption:(nullable NSString *)caption
albumMessageId:(nullable NSString *)albumMessageId
{
self = [super init];
if (!self) {
return self;
}
_dataSource = dataSource;
_contentType = contentType;
_sourceFilename = sourceFilename;
_caption = caption;
_albumMessageId = albumMessageId;
return self;
}
@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 `OWSMessageSender` to serialize message sending, ensuring that messages are emitted in the order they
* were sent.
*/
@interface OWSSendMessageOperation : OWSOperation
- (instancetype)init NS_UNAVAILABLE;
- (instancetype)initWithMessage:(TSOutgoingMessage *)message
messageSender:(OWSMessageSender *)messageSender
dbConnection:(YapDatabaseConnection *)dbConnection
success:(void (^)(void))aSuccessHandler
failure:(void (^)(NSError * error))aFailureHandler NS_DESIGNATED_INITIALIZER;
@end
#pragma mark -
@interface OWSMessageSender (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) OWSMessageSender *messageSender;
@property (nonatomic, readonly) YapDatabaseConnection *dbConnection;
@property (nonatomic, readonly) void (^successHandler)(void);
@property (nonatomic, readonly) void (^failureHandler)(NSError *error);
@end
#pragma mark -
@implementation OWSSendMessageOperation
- (instancetype)initWithMessage:(TSOutgoingMessage *)message
messageSender:(OWSMessageSender *)messageSender
dbConnection:(YapDatabaseConnection *)dbConnection
success:(void (^)(void))successHandler
failure:(void (^)(NSError * error))failureHandler
{
self = [super init];
if (!self) {
return self;
}
_message = message;
_messageSender = messageSender;
_dbConnection = dbConnection;
_successHandler = successHandler;
_failureHandler = failureHandler;
return self;
}
#pragma mark - OWSOperation overrides
- (nullable NSError *)checkForPreconditionError
{
__block NSError *_Nullable error = [super checkForPreconditionError];
if (error) { return error; }
if (self.message.hasAttachments) {
[self.dbConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
for (TSAttachment *attachment in [self.message attachmentsWithTransaction:transaction]) {
if (![attachment isKindOfClass:[TSAttachmentStream class]]) {
error = OWSErrorMakeFailedToSendOutgoingMessageError();
break;
}
TSAttachmentStream *attachmentStream = (TSAttachmentStream *)attachment;
OWSAssertDebug(attachmentStream);
OWSAssertDebug(attachmentStream.serverId);
OWSAssertDebug(attachmentStream.isUploaded);
}
}];
}
return error;
}
- (void)run
{
if (self.message.shouldBeSaved && ![TSOutgoingMessage fetchObjectWithUniqueID:self.message.uniqueId]) {
OWSLogInfo(@"Aborting message send; message deleted.");
NSError *error = OWSErrorWithCodeDescription(OWSErrorCodeMessageDeletedBeforeSent, @"Message was deleted before it could be sent.");
error.isFatal = YES;
[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) {
[LKLogger print:@"[Loki] Succeeded with sending a message, but the message state isn't TSOutgoingMessageStateSent."];
}
self.successHandler();
}
- (void)didFailWithError:(NSError *)error
{
OWSLogError(@"Message failed to send due to error: %@.", error);
self.failureHandler(error);
}
@end
#pragma mark -
NSString *const OWSMessageSenderInvalidDeviceException = @"InvalidDeviceException";
NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
@interface OWSMessageSender ()
@property (nonatomic, readonly) OWSPrimaryStorage *primaryStorage;
@property (nonatomic, readonly) YapDatabaseConnection *dbConnection;
@property (atomic, readonly) NSMutableDictionary<NSString *, NSOperationQueue *> *sendingQueueMap;
@end
#pragma mark -
@implementation OWSMessageSender
- (instancetype)initWithPrimaryStorage:(OWSPrimaryStorage *)primaryStorage
{
self = [super init];
if (!self) {
return self;
}
_primaryStorage = primaryStorage;
_sendingQueueMap = [NSMutableDictionary new];
_dbConnection = primaryStorage.newDatabaseConnection;
OWSSingletonAssert();
return self;
}
#pragma mark - Dependencies
- (id<ContactsManagerProtocol>)contactsManager
{
OWSAssertDebug(SSKEnvironment.shared.contactsManager);
return SSKEnvironment.shared.contactsManager;
}
- (OWSBlockingManager *)blockingManager
{
OWSAssertDebug(SSKEnvironment.shared.blockingManager);
return SSKEnvironment.shared.blockingManager;
}
- (TSNetworkManager *)networkManager
{
OWSAssertDebug(SSKEnvironment.shared.networkManager);
return SSKEnvironment.shared.networkManager;
}
- (id<OWSUDManager>)udManager
{
OWSAssertDebug(SSKEnvironment.shared.udManager);
return SSKEnvironment.shared.udManager;
}
- (TSAccountManager *)tsAccountManager
{
return TSAccountManager.sharedInstance;
}
- (OWSIdentityManager *)identityManager
{
return SSKEnvironment.shared.identityManager;
}
#pragma mark -
- (NSOperationQueue *)sendingQueueForMessage:(TSOutgoingMessage *)message
{
OWSAssertDebug(message);
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;
}
}
- (void)sendMessage:(TSOutgoingMessage *)message
success:(void (^)(void))successHandler
failure:(void (^)(NSError *error))failureHandler
{
OWSAssertDebug(message);
if (message.body.length > 0) {
OWSAssertDebug([message.body lengthOfBytesUsingEncoding:NSUTF8StringEncoding] <= kOversizeTextMessageSizeThreshold);
}
if (message.shouldBeSaved && !message.thread.isGroupThread && ![LKSessionMetaProtocol isThreadNoteToSelf:message.thread]) {
// Loki: Not strictly true but nice from a UI point of view
[NSNotificationCenter.defaultCenter postNotificationName:NSNotification.calculatingPoW object:[[NSNumber alloc] initWithUnsignedLongLong:message.timestamp]];
}
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
NSMutableArray<NSString *> *allAttachmentIds = [NSMutableArray new];
// This method will use a read/write transaction. This transaction
// will block until any open read/write transactions are complete.
//
// That's key - we don't want to send any messages in response
// to an incoming message until processing of that batch of messages
// is complete. For example, we wouldn't want to auto-reply to a
// group info request before that group info request's batch was
// finished processing. Otherwise, we might receive a delivery
// notice for a group update we hadn't yet saved to the database.
//
// So we're using YDB behavior to ensure this invariant, which is a bit
// unorthodox.
if (message.allAttachmentIds.count > 0) {
[LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
[allAttachmentIds addObjectsFromArray:[OutgoingMessagePreparer prepareMessageForSending:message transaction:transaction]];
} error:nil];
}
NSOperationQueue *sendingQueue = [self sendingQueueForMessage:message];
OWSSendMessageOperation *sendMessageOperation =
[[OWSSendMessageOperation alloc] initWithMessage:message
messageSender:self
dbConnection:self.dbConnection
success:successHandler
failure:failureHandler];
for (NSString *attachmentId in allAttachmentIds) {
OWSUploadOperation *uploadAttachmentOperation =
[[OWSUploadOperation alloc] initWithAttachmentId:attachmentId
threadID:message.thread.uniqueId
dbConnection:self.dbConnection];
[sendMessageOperation addDependency:uploadAttachmentOperation];
[sendingQueue addOperation:uploadAttachmentOperation];
}
[sendingQueue addOperation:sendMessageOperation];
});
}
- (void)sendTemporaryAttachment:(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);
[message remove];
};
void (^failureWithDeleteHandler)(NSError *error) = ^(NSError *error) {
failureHandler(error);
OWSLogDebug(@"Removing failed temporary attachment message with attachment ids: %@", message.attachmentIds);
[message remove];
};
[self sendAttachment:dataSource
contentType:contentType
sourceFilename:nil
albumMessageId:nil
inMessage:message
success:successWithDeleteHandler
failure:failureWithDeleteHandler];
}
- (void)sendAttachment:(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];
[self sendAttachments:@[ attachmentInfo, ]
inMessage:message
success:success
failure:failure];
}
- (void)sendAttachments:(NSArray<OWSOutgoingAttachmentInfo *> *)attachmentInfos
inMessage:(TSOutgoingMessage *)message
success:(void (^)(void))success
failure:(void (^)(NSError *error))failure
{
OWSAssertDebug(attachmentInfos.count > 0);
[OutgoingMessagePreparer prepareAttachments:attachmentInfos
inMessage:message
completionHandler:^(NSError *_Nullable error) {
if (error) {
failure(error);
return;
}
[self sendMessage:message success:success failure:failure];
}];
}
- (void)sendMessageToService:(TSOutgoingMessage *)message
success:(void (^)(void))success
failure:(RetryableFailureHandler)failure
{
[self.udManager ensureSenderCertificateWithSuccess:^(SMKSenderCertificate *senderCertificate) {
OWSAssertDebug(senderCertificate != nil);
dispatch_async(OWSDispatch.sendingQueue, ^{
[self sendMessageToService:message senderCertificate:senderCertificate success:success failure:failure];
});
}
failure:^(NSError *error) { // Should never occur
dispatch_async(OWSDispatch.sendingQueue, ^{
[self sendMessageToService:message senderCertificate:nil success:success failure:failure];
});
}];
}
- (nullable NSArray<NSString *> *)unsentRecipientsForMessage:(TSOutgoingMessage *)message
thread:(nullable TSThread *)thread
error:(NSError **)errorHandle
{
OWSAssertDebug(message);
OWSAssertDebug(errorHandle);
NSString *userPublicKey = self.tsAccountManager.localNumber;
__block NSMutableSet<NSString *> *recipientIds = [NSMutableSet new];
if ([message isKindOfClass:OWSOutgoingSyncMessage.class]) {
recipientIds = [LKSessionMetaProtocol getDestinationsForOutgoingSyncMessage];
} else if (thread.isGroupThread) {
TSGroupThread *groupThread = (TSGroupThread *)thread;
recipientIds = [LKSessionMetaProtocol getDestinationsForOutgoingGroupMessage:message inThread:thread];
__block NSString *userMasterPublicKey;
[OWSPrimaryStorage.sharedManager.dbReadConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
userMasterPublicKey = [LKDatabaseUtilities getMasterHexEncodedPublicKeyFor:userPublicKey in:transaction] ?: userPublicKey;
}];
if ([recipientIds containsObject:userMasterPublicKey]) {
OWSFailDebug(@"Message send recipients should not include self.");
}
} else if ([thread isKindOfClass:TSContactThread.class]) {
NSString *recipientContactId = ((TSContactThread *)thread).contactIdentifier;
// Treat 1:1 sends to blocked contacts as failures.
// If we block a user, don't send 1:1 messages to them. The UI
// should prevent this from occurring, but in some edge cases
// you might, for example, have a pending outgoing message when
// you block them.
OWSAssertDebug(recipientContactId.length > 0);
if ([self.blockingManager isRecipientIdBlocked:recipientContactId]) {
OWSLogInfo(@"Skipping 1:1 send to blocked contact: %@", recipientContactId);
NSError *error = OWSErrorMakeMessageSendFailedDueToBlockListError();
[error setIsRetryable:NO];
*errorHandle = error;
return nil;
}
[recipientIds addObject:recipientContactId];
} else {
OWSFailDebug(@"Unknown message type: %@", [message class]);
NSError *error = OWSErrorMakeFailedToSendOutgoingMessageError();
[error setIsRetryable:NO];
*errorHandle = error;
return nil;
}
[recipientIds minusSet:[NSSet setWithArray:self.blockingManager.blockedPhoneNumbers]];
return recipientIds.allObjects;
}
- (NSArray<SignalRecipient *> *)recipientsForRecipientIds:(NSArray<NSString *> *)recipientIds
{
OWSAssertDebug(recipientIds.count > 0);
NSMutableArray<SignalRecipient *> *recipients = [NSMutableArray new];
[self.dbConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
for (NSString *recipientId in recipientIds) {
SignalRecipient *recipient =
[SignalRecipient getOrBuildUnsavedRecipientForRecipientId:recipientId transaction:transaction];
[recipients addObject:recipient];
}
}];
return [recipients copy];
}
- (AnyPromise *)sendPromiseForRecipients:(NSArray<SignalRecipient *> *)recipients
message:(TSOutgoingMessage *)message
thread:(nullable TSThread *)thread
senderCertificate:(nullable SMKSenderCertificate *)senderCertificate
sendErrors:(NSMutableArray<NSError *> *)sendErrors
{
OWSAssertDebug(recipients.count > 0);
OWSAssertDebug(message);
OWSAssertDebug(sendErrors);
NSMutableArray<AnyPromise *> *sendPromises = [NSMutableArray array];
for (SignalRecipient *recipient in recipients) {
AnyPromise *sendPromise = [AnyPromise promiseWithResolverBlock:^(PMKResolver resolve) {
NSString *localNumber = self.tsAccountManager.localNumber;
OWSUDAccess *_Nullable theirUDAccess;
if (senderCertificate != nil && ![recipient.recipientId isEqualToString:localNumber]) {
theirUDAccess = [self.udManager udAccessForRecipientId:recipient.recipientId requireSyncAccess:YES];
}
OWSMessageSend *messageSend = [[OWSMessageSend alloc] initWithMessage:message
thread:thread
recipient:recipient
senderCertificate:senderCertificate
udAccess:theirUDAccess
localNumber:self.tsAccountManager.localNumber
success:^{
// The value doesn't matter, we just need any non-NSError value.
resolve(@(1));
}
failure:^(NSError *error) {
@synchronized(sendErrors) {
[sendErrors addObject:error];
}
resolve(error);
}];
// NSString *publicKey = recipients.firstObject.recipientId;
// if ([LKMultiDeviceProtocol isMultiDeviceRequiredForMessage:message toPublicKey:publicKey]) { // Avoid the write transaction if possible
// [self.primaryStorage.dbReadConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
// [LKMultiDeviceProtocol sendMessageToDestinationAndLinkedDevices:messageSend transaction:transaction];
// }];
// } else {
[self sendMessage:messageSend];
// }
}];
[sendPromises addObject:sendPromise];
}
// 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
senderCertificate:(nullable SMKSenderCertificate *)senderCertificate
success:(void (^)(void))successHandlerParam
failure:(RetryableFailureHandler)failureHandlerParam
{
AssertIsOnSendingQueue();
OWSAssert(senderCertificate);
void (^successHandler)(void) = ^() {
dispatch_async(OWSDispatch.sendingQueue, ^{
[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) {
dispatch_async(OWSDispatch.sendingQueue, ^{
[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 *_Nullable thread = message.thread;
BOOL isSyncMessage = [message isKindOfClass:[OWSOutgoingSyncMessage class]];
if (thread == nil && !isSyncMessage) {
OWSFailDebug(@"Missing thread for non-sync message.");
// The thread has been deleted since the message was enqueued.
NSError *error = OWSErrorWithCodeDescription(OWSErrorCodeMessageSendNoValidRecipients,
NSLocalizedString(@"ERROR_DESCRIPTION_NO_VALID_RECIPIENTS", @"Error indicating that an outgoing message had no valid recipients."));
[error setIsRetryable:NO];
return failureHandler(error);
}
// In the "self-send" special case, we ony need to send a sync message with a delivery receipt
// Loki: Take into account multi device
if ([LKSessionMetaProtocol isThreadNoteToSelf:thread]
&& !([message isKindOfClass:LKDeviceLinkMessage.class]) && !([message isKindOfClass:LKClosedGroupUpdateMessage.class])) {
// Don't mark self-sent messages as read (or sent) until the sync transcript is sent
successHandler();
return;
}
if (thread.isGroupThread) {
[self saveInfoMessageForGroupMessage:message inThread:thread];
}
NSError *error;
NSArray<NSString *> *_Nullable recipientIds = [self unsentRecipientsForMessage:message thread:thread error:&error];
if (error || !recipientIds) {
error = SSKEnsureError(
error, OWSErrorCodeMessageSendNoValidRecipients, @"Couldn't build recipient list for message.");
[error setIsRetryable:NO];
return failureHandler(error);
}
// Mark skipped recipients as such. We skip because:
//
// * Recipient is no longer in the group.
// * Recipient is blocked.
//
// Elsewhere, we skip recipient if their Signal account has been deactivated.
NSMutableSet<NSString *> *obsoleteRecipientIds = [NSMutableSet setWithArray:message.sendingRecipientIds];
[obsoleteRecipientIds minusSet:[NSSet setWithArray:recipientIds]];
if (obsoleteRecipientIds.count > 0) {
[LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
for (NSString *recipientId in obsoleteRecipientIds) {
[message updateWithSkippedRecipient:recipientId transaction:transaction];
}
} error:nil];
}
if (recipientIds.count < 1) {
successHandler();
return;
}
NSArray<SignalRecipient *> *recipients = [self recipientsForRecipientIds:recipientIds];
BOOL isGroupSend = (thread && thread.isGroupThread);
NSMutableArray<NSError *> *sendErrors = [NSMutableArray array];
AnyPromise *sendPromise = [self sendPromiseForRecipients:recipients
message:message
thread:thread
senderCertificate:senderCertificate
sendErrors:sendErrors]
.then(^(id value) {
successHandler();
});
sendPromise.catch(^(id failure) {
NSError *firstRetryableError = nil;
NSError *firstNonRetryableError = nil;
NSArray<NSError *> *sendErrorsCopy;
@synchronized(sendErrors) {
sendErrorsCopy = [sendErrors copy];
}
for (NSError *error in sendErrorsCopy) {
// Some errors should be ignored when sending messages
// to groups. See discussion on
// NSError (OWSMessageSender) 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.isFatal) {
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 = OWSErrorWithCodeDescription(OWSErrorCodeMessageSendNoValidRecipients,
NSLocalizedString(@"ERROR_DESCRIPTION_NO_VALID_RECIPIENTS", @"Error indicating that an outgoing message had no valid recipients."));
[error setIsRetryable:NO];
failureHandler(error);
} else {
successHandler();
}
}
});
[sendPromise retainUntilComplete];
}
- (nullable NSArray<NSDictionary *> *)deviceMessagesForMessageSend:(OWSMessageSend *)messageSend
error:(NSError **)errorHandle
{
OWSAssertDebug(messageSend);
OWSAssertDebug(errorHandle);
AssertIsOnSendingQueue();
SignalRecipient *recipient = messageSend.recipient;
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 = OWSErrorWithCodeDescription(
OWSErrorCodeNoSessionForTransientMessage, @"No session for transient message.");
[error setIsRetryable:NO];
[error setIsFatal:YES];
*errorHandle = error;
return nil;
} else if ([exception.name isEqualToString:UntrustedIdentityKeyException]) {
// 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]);
NSString *localizedErrorDescriptionFormat
= NSLocalizedString(@"FAILED_SENDING_BECAUSE_UNTRUSTED_IDENTITY_KEY",
@"action sheet header when re-sending message which failed because of untrusted identity keys");
NSString *localizedErrorDescription =
[NSString stringWithFormat:localizedErrorDescriptionFormat,
[self.contactsManager displayNameForPhoneIdentifier:recipient.recipientId]];
NSError *error = OWSErrorMakeUntrustedIdentityError(localizedErrorDescription, recipient.recipientId);
// Key will continue to be unaccepted, so no need to retry. It'll only cause us to hit the Pre-Key request
// rate limit
[error setIsRetryable:NO];
// Avoid the "Too many failures with this contact" error rate limiting.
[error setIsFatal:YES];
*errorHandle = error;
PreKeyBundle *_Nullable newKeyBundle = exception.userInfo[TSInvalidPreKeyBundleKey];
if (newKeyBundle == nil) {
OWSProdFail([OWSAnalyticsEvents messageSenderErrorMissingNewPreKeyBundle]);
return nil;
}
if (![newKeyBundle isKindOfClass:[PreKeyBundle class]]) {
OWSProdFail([OWSAnalyticsEvents messageSenderErrorUnexpectedKeyBundle]);
return nil;
}
NSData *newIdentityKeyWithVersion = newKeyBundle.identityKey;
if (![newIdentityKeyWithVersion isKindOfClass:[NSData class]]) {
OWSProdFail([OWSAnalyticsEvents messageSenderErrorInvalidIdentityKeyType]);
return nil;
}
// TODO migrate to storing the full 33 byte representation of the identity key.
if (newIdentityKeyWithVersion.length != kIdentityKeyLength) {
OWSProdFail([OWSAnalyticsEvents messageSenderErrorInvalidIdentityKeyLength]);
return nil;
}
NSData *newIdentityKey = [newIdentityKeyWithVersion throws_removeKeyType];
[self.identityManager saveRemoteIdentity:newIdentityKey recipientId:recipient.recipientId];
return nil;
}
if ([exception.name isEqualToString:OWSMessageSenderRateLimitedException]) {
NSError *error = OWSErrorWithCodeDescription(OWSErrorCodeSignalServiceRateLimited,
NSLocalizedString(@"FAILED_SENDING_BECAUSE_RATE_LIMIT",
@"action sheet header when re-sending message which failed because of too many attempts"));
// We're already rate-limited. No need to exacerbate the problem.
[error setIsRetryable:NO];
// Avoid exacerbating the rate limiting.
[error setIsFatal:YES];
*errorHandle = error;
return nil;
}
OWSLogWarn(@"Could not build device messages: %@", exception);
NSError *error = OWSErrorMakeFailedToSendOutgoingMessageError();
[error setIsRetryable:YES];
*errorHandle = error;
return nil;
}
return deviceMessages;
}
- (void)sendMessage:(OWSMessageSend *)messageSend
{
OWSAssertDebug(messageSend);
OWSAssertDebug(messageSend.thread || [messageSend.message isKindOfClass:[OWSOutgoingSyncMessage class]]);
NSString *userPublicKey = OWSIdentityManager.sharedManager.identityKeyPair.hexEncodedPublicKey;
if (!messageSend.isUDSend && ![messageSend.recipient.recipientId isEqual:userPublicKey]) {
[LKLogger print:@"[Loki] Non-UD send"];
}
TSOutgoingMessage *message = messageSend.message;
SignalRecipient *recipient = messageSend.recipient;
OWSLogInfo(@"Attempting to send message: %@, timestamp: %llu, recipient: %@.",
message.class,
message.timestamp,
recipient.uniqueId);
AssertIsOnSendingQueue();
if ([TSPreKeyManager isAppLockedDueToPreKeyUpdateFailures]) {
OWSProdError([OWSAnalyticsEvents messageSendErrorFailedDueToPrekeyUpdateFailures]);
// Retry pre key update every time user tries to send a message while the app
// is disabled due to pre key update failures.
//
// Only try to update the signed pre key; updating it is sufficient to
// re-enable message sending.
[TSPreKeyManager
rotateSignedPreKeyWithSuccess:^{
OWSLogInfo(@"New pre keys registered with server.");
NSError *error = OWSErrorMakeMessageSendDisabledDueToPreKeyUpdateFailuresError();
[error setIsRetryable:YES];
return messageSend.failure(error);
}
failure:^(NSError *error) {
OWSLogWarn(@"Failed to update pre keys with the server due to error: %@.", error);
return messageSend.failure(error);
}];
}
if (messageSend.remainingAttempts <= 0) {
// We should always fail with a specific error.
OWSProdFail([OWSAnalyticsEvents messageSenderErrorGenericSendFailure]);
NSError *error = OWSErrorMakeFailedToSendOutgoingMessageError();
[error setIsRetryable:YES];
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 isKindOfClass:[OWSOutgoingSyncMessage class]]
&& ![message isKindOfClass:[OWSOutgoingSentMessageTranscript class]]) {
[messageSend disableUD];
}
NSError *deviceMessagesError;
NSArray<NSDictionary *> *_Nullable deviceMessages;
if (message.thread.isGroupThread && ((TSGroupThread *)message.thread).isPublicChat) {
deviceMessages = @{};
} else {
deviceMessages = [self deviceMessagesForMessageSend:messageSend error:&deviceMessagesError];
// Loki: Remove this when we have shared sender keys
// ========
if (deviceMessages.count == 0) {
return messageSend.success();
}
// ========
}
if (deviceMessagesError || !deviceMessages) {
OWSAssertDebug(deviceMessagesError);
return messageSend.failure(deviceMessagesError);
}
for (NSDictionary *deviceMessage in deviceMessages) {
NSNumber *_Nullable messageType = deviceMessage[@"type"];
OWSAssertDebug(messageType);
BOOL hasValidMessageType;
if (messageSend.isUDSend) {
hasValidMessageType = [messageType isEqualToNumber:@(TSUnidentifiedSenderMessageType)];
} else {
NSArray *validMessageTypes = @[ @(TSEncryptedWhisperMessageType), @(TSPreKeyWhisperMessageType), @(TSFallbackMessageType), @(TSClosedGroupCiphertextMessageType) ];
hasValidMessageType = [validMessageTypes containsObject:messageType];
}
if (!hasValidMessageType) {
OWSFailDebug(@"Invalid message type: %@.", messageType);
NSError *error = OWSErrorMakeFailedToSendOutgoingMessageError();
[error setIsRetryable:NO];
return messageSend.failure(error);
}
}
if (deviceMessages.count == 0 && !(message.thread.isGroupThread && ((TSGroupThread *)message.thread).isPublicChat)) {
// This might happen:
//
// * The first (after upgrading?) time we send a sync message to our linked devices.
// * After unlinking all linked devices.
// * After trying and failing to link a device.
// * The first time we send a message to a user, if they don't have their
// default device. For example, if they have unregistered
// their primary but still have a linked device. Or later, when they re-register.
//
// When we're not sure if we have linked devices, we need to try
// to send self-sync messages even if they have no device messages
// so that we can learn from the service whether or not there are
// linked devices that we don't know about.
OWSLogWarn(@"Sending a message with no device messages.");
NSError *error = OWSErrorMakeFailedToSendOutgoingMessageError();
[error setIsRetryable:NO];
return messageSend.failure(error);
}
void (^failedMessageSend)(NSError *error) = ^(NSError *error) {
NSUInteger statusCode = 0;
NSData *_Nullable responseData = nil;
if ([error.domain isEqualToString:TSNetworkManagerErrorDomain]) {
statusCode = error.code;
NSError *_Nullable underlyingError = error.userInfo[NSUnderlyingErrorKey];
if (underlyingError) {
responseData = underlyingError.userInfo[AFNetworkingOperationFailingURLResponseDataErrorKey];
} else {
OWSFailDebug(@"Missing underlying error: %@.", error);
}
}
[self messageSendDidFail:messageSend deviceMessages:deviceMessages statusCode:statusCode error:error responseData:responseData];
};
__block LKPublicChat *publicChat;
[self.dbConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
publicChat = [LKDatabaseUtilities getPublicChatForThreadID:message.uniqueThreadId transaction: transaction];
}];
if (publicChat != nil) {
NSString *userPublicKey = OWSIdentityManager.sharedManager.identityKeyPair.hexEncodedPublicKey;
NSString *displayName = SSKEnvironment.shared.profileManager.localProfileName;
if (displayName == nil) { displayName = @"Anonymous"; }
TSQuotedMessage *quote = message.quotedMessage;
uint64_t quoteID = quote.timestamp;
NSString *quoteePublicKey = quote.authorId;
__block uint64_t quotedMessageServerID = 0;
if (quoteID != 0) {
[self.dbConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
quotedMessageServerID = [LKDatabaseUtilities getServerIDForQuoteWithID:quoteID quoteeHexEncodedPublicKey:quoteePublicKey threadID:messageSend.thread.uniqueId transaction:transaction];
}];
}
NSString *body = (message.body != nil && message.body.length > 0) ? message.body : [NSString stringWithFormat:@"%@", @(message.timestamp)]; // Workaround for the fact that the back-end doesn't accept messages without a body
LKPublicChatMessage *groupMessage = [[LKPublicChatMessage alloc] initWithSenderPublicKey:userPublicKey displayName:displayName body:body type:LKPublicChatAPI.publicChatMessageType
timestamp:message.timestamp quotedMessageTimestamp:quoteID quoteePublicKey:quoteePublicKey quotedMessageBody:quote.body quotedMessageServerID:quotedMessageServerID signatureData:nil signatureVersion:0 serverTimestamp:0];
OWSLinkPreview *linkPreview = message.linkPreview;
if (linkPreview != nil) {
TSAttachmentStream *attachment = [TSAttachmentStream fetchObjectWithUniqueID:linkPreview.imageAttachmentId];
if (attachment != nil) {
[groupMessage addAttachmentWithKind:@"preview" server:publicChat.server serverID:attachment.serverId contentType:attachment.contentType size:attachment.byteCount fileName:attachment.sourceFilename flags:0 width:@(attachment.imageSize.width).unsignedIntegerValue height:@(attachment.imageSize.height).unsignedIntegerValue caption:attachment.caption url:attachment.downloadURL linkPreviewURL:linkPreview.urlString linkPreviewTitle:linkPreview.title];
}
}
for (NSString *attachmentID in message.attachmentIds) {
TSAttachmentStream *attachment = [TSAttachmentStream fetchObjectWithUniqueID:attachmentID];
if (attachment == nil) { continue; }
NSUInteger width = attachment.shouldHaveImageSize ? @(attachment.imageSize.width).unsignedIntegerValue : 0;
NSUInteger height = attachment.shouldHaveImageSize ? @(attachment.imageSize.height).unsignedIntegerValue : 0;
[groupMessage addAttachmentWithKind:@"attachment" server:publicChat.server serverID:attachment.serverId contentType:attachment.contentType size:attachment.byteCount fileName:attachment.sourceFilename flags:0 width:width height:height caption:attachment.caption url:attachment.downloadURL linkPreviewURL:nil linkPreviewTitle:nil];
}
message.actualSenderHexEncodedPublicKey = userPublicKey;
[[LKPublicChatAPI sendMessage:groupMessage toGroup:publicChat.channel onServer:publicChat.server]
.thenOn(OWSDispatch.sendingQueue, ^(LKPublicChatMessage *groupMessage) {
[LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
[message saveOpenGroupServerMessageID:groupMessage.serverID in:transaction];
[self.primaryStorage setIDForMessageWithServerID:groupMessage.serverID to:message.uniqueId in:transaction];
} error:nil];
[self messageSendDidSucceed:messageSend deviceMessages:deviceMessages wasSentByUD:messageSend.isUDSend wasSentByWebsocket:false];
})
.catchOn(OWSDispatch.sendingQueue, ^(NSError *error) {
failedMessageSend(error);
}) retainUntilComplete];
} else {
NSString *targetPublicKey = recipient.recipientId;
NSString *userPublicKey = OWSIdentityManager.sharedManager.identityKeyPair.hexEncodedPublicKey;
__block BOOL isUserLinkedDevice;
[self.dbConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
isUserLinkedDevice = [LKDatabaseUtilities isUserLinkedDevice:targetPublicKey in:transaction];
}];
BOOL isSSKBasedClosedGroup = [messageSend.thread isKindOfClass:TSGroupThread.class] && ((TSGroupThread *)messageSend.thread).usesSharedSenderKeys;
if (isSSKBasedClosedGroup) {
[LKLogger print:[NSString stringWithFormat:@"[Loki] Sending %@ to SSK based closed group.", message.class]];
} else if ([targetPublicKey isEqual:userPublicKey]) {
[LKLogger print:[NSString stringWithFormat:@"[Loki] Sending %@ to self.", message.class]];
} else if (isUserLinkedDevice) {
[LKLogger print:[NSString stringWithFormat:@"[Loki] Sending %@ to %@ (one of the current user's linked devices).", message.class, recipient.recipientId]];
} else {
[LKLogger print:[NSString stringWithFormat:@"[Loki] Sending %@ to %@.", message.class, recipient.recipientId]];
}
NSDictionary *signalMessageInfo = deviceMessages.firstObject;
SSKProtoEnvelopeType type = ((NSNumber *)signalMessageInfo[@"type"]).integerValue;
uint32_t senderDeviceID = (type == SSKProtoEnvelopeTypeUnidentifiedSender) ? 0 : OWSDevicePrimaryDeviceId;
NSString *content = signalMessageInfo[@"content"];
NSString *recipientID = signalMessageInfo[@"destination"];
uint64_t ttl = ((NSNumber *)signalMessageInfo[@"ttl"]).unsignedIntegerValue;
BOOL isPing = ((NSNumber *)signalMessageInfo[@"isPing"]).boolValue;
uint64_t timestamp = message.timestamp;
NSString *senderID;
if (type == SSKProtoEnvelopeTypeClosedGroupCiphertext) {
senderID = recipientID;
} else if (type == SSKProtoEnvelopeTypeUnidentifiedSender) {
senderID = @"";
} else {
senderID = userPublicKey;
[LKLogger print:@"[Loki] Non-UD send"];
}
LKSignalMessage *signalMessage = [[LKSignalMessage alloc] initWithType:type timestamp:timestamp senderID:senderID senderDeviceID:senderDeviceID content:content recipientID:recipientID ttl:ttl isPing:isPing];
[LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
if (!message.skipSave) {
// Update the PoW calculation status
[message saveIsCalculatingProofOfWork:YES withTransaction:transaction];
}
} error:nil];
// Convenience
void (^handleError)(NSError *error) = ^(NSError *error) {
[LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
if (!message.skipSave) {
// Update the PoW calculation status
[message saveIsCalculatingProofOfWork:NO withTransaction:transaction];
}
} error:nil];
// Handle the error
failedMessageSend(error);
};
// Send the message
[[LKSnodeAPI sendSignalMessage:signalMessage]
.thenOn(OWSDispatch.sendingQueue, ^(id result) {
NSSet<AnyPromise *> *promises = (NSSet<AnyPromise *> *)result;
__block BOOL isSuccess = NO;
NSUInteger promiseCount = promises.count;
__block NSUInteger errorCount = 0;
for (AnyPromise *promise in promises) {
[promise
.thenOn(OWSDispatch.sendingQueue, ^(id result) {
if (isSuccess) { return; } // Succeed as soon as the first promise succeeds
[NSNotificationCenter.defaultCenter postNotificationName:NSNotification.messageSent object:[[NSNumber alloc] initWithUnsignedLongLong:signalMessage.timestamp]];
isSuccess = YES;
[self messageSendDidSucceed:messageSend deviceMessages:deviceMessages wasSentByUD:messageSend.isUDSend wasSentByWebsocket:false];
})
.catchOn(OWSDispatch.sendingQueue, ^(NSError *error) {
errorCount += 1;
if (errorCount != promiseCount) { return; } // Only error out if all promises failed
[NSNotificationCenter.defaultCenter postNotificationName:NSNotification.messageFailed object:[[NSNumber alloc] initWithUnsignedLongLong:signalMessage.timestamp]];
handleError(error);
}) retainUntilComplete];
}
})
.catchOn(OWSDispatch.sendingQueue, ^(NSError *error) {
handleError(error);
}) retainUntilComplete];
}
}
- (void)messageSendDidSucceed:(OWSMessageSend *)messageSend
deviceMessages:(NSArray<NSDictionary *> *)deviceMessages
wasSentByUD:(BOOL)wasSentByUD
wasSentByWebsocket:(BOOL)wasSentByWebsocket
{
OWSAssertDebug(messageSend);
OWSAssertDebug(deviceMessages);
SignalRecipient *recipient = messageSend.recipient;
OWSLogInfo(@"Successfully sent message: %@ timestamp: %llu, wasSentByUD: %d.",
messageSend.message.class, messageSend.message.timestamp, wasSentByUD);
if (messageSend.isLocalNumber && deviceMessages.count == 0) {
OWSLogInfo(@"Sent a message with no device messages; clearing 'mayHaveLinkedDevices'.");
// In order to avoid skipping necessary sync messages, the default value
// for mayHaveLinkedDevices is YES. Once we've successfully sent a
// sync message with no device messages (e.g. the service has confirmed
// that we have no linked devices), we can set mayHaveLinkedDevices to NO
// to avoid unnecessary message sends for sync messages until we learn
// of a linked device (e.g. through the device linking UI or by receiving
// a sync message, etc.).
[OWSDeviceManager.sharedManager clearMayHaveLinkedDevices];
}
dispatch_async(OWSDispatch.sendingQueue, ^{
[LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
[messageSend.message updateWithSentRecipient:messageSend.recipient.uniqueId
wasSentByUD:wasSentByUD
transaction:transaction];
// If we've just delivered a message to a user, we know they
// have a valid Signal account.
[SignalRecipient markRecipientAsRegisteredAndGet:recipient.recipientId transaction:transaction];
} error:nil];
messageSend.success();
});
}
- (void)messageSendDidFail:(OWSMessageSend *)messageSend
deviceMessages:(NSArray<NSDictionary *> *)deviceMessages
statusCode:(NSInteger)statusCode
error:(NSError *)responseError
responseData:(nullable NSData *)responseData
{
OWSAssertDebug(messageSend);
OWSAssertDebug(messageSend.thread || [messageSend.message isKindOfClass:[OWSOutgoingSyncMessage class]]);
OWSAssertDebug(deviceMessages);
OWSAssertDebug(responseError);
TSOutgoingMessage *message = messageSend.message;
SignalRecipient *recipient = messageSend.recipient;
OWSLogInfo(@"Failed to send message: %@, timestamp: %llu, to recipient: %@.",
message.class,
message.timestamp,
recipient.uniqueId);
void (^retrySend)(void) = ^void() {
if (messageSend.remainingAttempts <= 0) {
return messageSend.failure(responseError);
}
dispatch_async(OWSDispatch.sendingQueue, ^{
OWSLogDebug(@"Retrying: %@.", message.debugDescription);
[self sendMessage:messageSend];
});
};
switch (statusCode) {
case 0: { // Loki
NSError *error;
if ([responseError isKindOfClass:LKSnodeAPIError.class] || [responseError isKindOfClass:LKDotNetAPIError.class]
|| [responseError isKindOfClass:DiffieHellmanError.class]) {
error = responseError;
} else {
error = OWSErrorMakeFailedToSendOutgoingMessageError();
}
[error setIsRetryable:NO];
return messageSend.failure(error);
}
case 401: {
OWSLogWarn(@"Unable to send due to invalid credentials. Did the user's client get de-authed by "
@"registering elsewhere?");
NSError *error = OWSErrorWithCodeDescription(OWSErrorCodeSignalServiceFailure,
NSLocalizedString(@"ERROR_DESCRIPTION_SENDING_UNAUTHORIZED", @"Error message when attempting to send message"));
// No need to retry if we've been de-authed.
[error setIsRetryable:NO];
return messageSend.failure(error);
}
default:
retrySend();
break;
}
}
- (void)handleMessageSentLocally:(TSOutgoingMessage *)message
success:(void (^)(void))successParam
failure:(RetryableFailureHandler)failure
{
dispatch_block_t success = ^{
// Don't mark self-sent messages as read (or sent) until the sync transcript is sent
// Loki: Take into account multi device
BOOL isNoteToSelf = [LKSessionMetaProtocol isThreadNoteToSelf:message.thread];
if (isNoteToSelf && !([message isKindOfClass:LKDeviceLinkMessage.class])
&& ![message isKindOfClass:LKClosedGroupUpdateMessage.class]) {
[LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
for (NSString *recipientId in message.sendingRecipientIds) {
[message updateWithReadRecipientId:recipientId readTimestamp:message.timestamp transaction:transaction];
}
} error:nil];
}
successParam();
};
[LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
[[OWSDisappearingMessagesJob sharedJob] startAnyExpirationForMessage:message
expirationStartedAt:[NSDate ows_millisecondTimeStamp]
transaction:transaction];
} error:nil];
if (!message.shouldSyncTranscript) {
return success();
}
BOOL shouldSendTranscript = [LKSessionMetaProtocol shouldSendTranscriptForMessage:message inThread:message.thread];
if (!shouldSendTranscript) {
return success();
}
BOOL isRecipientUpdate = message.hasSyncedTranscript;
[self
sendSyncTranscriptForMessage:message
isRecipientUpdate:isRecipientUpdate
success:^{
[LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
[message updateWithHasSyncedTranscript:YES transaction:transaction];
} error:nil];
success();
}
failure:failure];
}
- (void)sendSyncTranscriptForMessage:(TSOutgoingMessage *)message
isRecipientUpdate:(BOOL)isRecipientUpdate
success:(void (^)(void))success
failure:(RetryableFailureHandler)failure
{
OWSOutgoingSentMessageTranscript *sentMessageTranscript =
[[OWSOutgoingSentMessageTranscript alloc] initWithOutgoingMessage:message isRecipientUpdate:isRecipientUpdate];
NSString *userPublicKey = self.tsAccountManager.localNumber;
// Loki: Send to the user's other device
__block NSSet<NSString *> *userLinkedDevices;
[self.primaryStorage.dbReadConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
userLinkedDevices = [LKDatabaseUtilities getLinkedDeviceHexEncodedPublicKeysFor:userPublicKey in:transaction];
}];
NSString *otherUserDevice;
for (NSString *device in userLinkedDevices) {
if (![device isEqual:userPublicKey]) {
otherUserDevice = device;
break;
}
}
NSString *recipientId = otherUserDevice ?: userPublicKey;
__block SignalRecipient *recipient;
[LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
recipient = [SignalRecipient markRecipientAsRegisteredAndGet:recipientId transaction:transaction];
} error:nil];
SMKSenderCertificate *senderCertificate = [self.udManager getSenderCertificate];
OWSUDAccess *recipientUDAccess = nil;
if (senderCertificate != nil) {
recipientUDAccess = [self.udManager udAccessForRecipientId:recipient.recipientId requireSyncAccess:YES];
}
// Loki: If the message was aimed at an SSK based closed group, aim the sync transcript at
// the contact thread with the other device rather than also sending it to the group.
__block TSThread *thread = message.thread;
if ([thread isKindOfClass:TSGroupThread.class] && ((TSGroupThread *)thread).usesSharedSenderKeys) {
[LKStorage readWithBlock:^(YapDatabaseReadTransaction *transaction) {
thread = [TSContactThread getThreadWithContactId:otherUserDevice transaction:transaction];
}];
}
OWSMessageSend *messageSend = [[OWSMessageSend alloc] initWithMessage:sentMessageTranscript
thread:thread
recipient:recipient
senderCertificate:senderCertificate
udAccess:recipientUDAccess
localNumber:self.tsAccountManager.localNumber
success:^{
OWSLogInfo(@"Successfully sent sync transcript.");
success();
}
failure:^(NSError *error) {
OWSLogInfo(@"Failed to send sync transcript: %@ (isRetryable: %d).", error, error.isRetryable);
failure(error);
}];
[self sendMessage:messageSend];
}
- (NSArray<NSDictionary *> *)throws_deviceMessagesForMessageSend:(OWSMessageSend *)messageSend
{
// Loki: Multi device is handled elsewhere so just send to the provided recipient ID (Signal used
// to send to each of the recipient's devices here)
OWSAssertDebug(messageSend.message != nil);
OWSAssertDebug(messageSend.recipient != nil);
SignalRecipient *recipient = messageSend.recipient;
NSMutableArray *messagesArray = [NSMutableArray new];
NSData *_Nullable plainText = [messageSend.message buildPlainTextData:recipient];
if (!plainText) {
OWSRaiseException(InvalidMessageException, @"Failed to build message proto.");
}
OWSLogDebug(@"Built message: %@ plainTextData.length: %lu", [messageSend.message class], (unsigned long)plainText.length);
NSArray<NSString *> *recipientID = recipient.recipientId;
OWSLogVerbose(@"Building device messages for: %@ %@ (isLocalNumber: %d, isUDSend: %d).",
recipientID,
recipient.devices,
messageSend.isLocalNumber,
messageSend.isUDSend);
@try {
__block BOOL isSessionRequired;
[LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
isSessionRequired = [LKSessionManagementProtocol isSessionRequiredForMessage:messageSend.message recipientID:recipientID transaction:transaction];
} error:nil];
if (isSessionRequired) {
BOOL hasSession = [self throws_ensureRecipientHasSessionForMessageSend:messageSend recipientID:recipientID deviceId:@(OWSDevicePrimaryDeviceId)];
// Loki: Remove this when shared sender keys has been widely rolled out
// ========
if (!hasSession && [LKSessionManagementProtocol shouldIgnoreMissingPreKeyBundleExceptionForMessage:messageSend.message to:recipientID]) {
return [NSDictionary new];
}
// ========
}
__block NSDictionary *_Nullable messageDict;
__block NSException *encryptionException;
[LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
@try {
messageDict = [self throws_encryptedMessageForMessageSend:messageSend
recipientID:recipientID
plainText:plainText
transaction:transaction];
} @catch (NSException *exception) {
encryptionException = exception;
}
} error:nil];
if (encryptionException) {
OWSLogInfo(@"Exception during encryption: %@.", encryptionException);
@throw encryptionException;
}
if (messageDict) {
[messagesArray addObject:messageDict];
} else {
OWSRaiseException(InvalidMessageException, @"Failed to encrypt message.");
}
} @catch (NSException *exception) {
if ([exception.name isEqualToString:OWSMessageSenderInvalidDeviceException]) {
[LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
[recipient updateRegisteredRecipientWithDevicesToAdd:nil
devicesToRemove:@[ @(OWSDevicePrimaryDeviceId) ]
transaction:transaction];
} error:nil];
} else {
@throw exception;
}
}
return [messagesArray copy];
}
- (BOOL)throws_ensureRecipientHasSessionForMessageSend:(OWSMessageSend *)messageSend recipientID:(NSString *)recipientID deviceId:(NSNumber *)deviceId
{
OWSAssertDebug(messageSend);
OWSAssertDebug(deviceId);
OWSPrimaryStorage *storage = self.primaryStorage;
SignalRecipient *recipient = messageSend.recipient;
OWSAssertDebug(recipientID.length > 0);
// 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.");
}
PreKeyBundle *_Nullable bundle = [storage getPreKeyBundleForContact:recipientID];
__block NSException *exception;
if (!bundle) {
__block BOOL hasSession;
[LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
hasSession = [storage containsSession:recipientID deviceId:[deviceId intValue] protocolContext:transaction];
} error:nil];
if (hasSession) { return YES; }
TSOutgoingMessage *message = messageSend.message;
// Loki: Remove this when we have shared sender keys
// ========
if ([LKSessionManagementProtocol shouldIgnoreMissingPreKeyBundleExceptionForMessage:message to:recipientID]) { return NO; }
// ========
NSString *missingPrekeyBundleException = @"missingPrekeyBundleException";
OWSRaiseException(missingPrekeyBundleException, @"Missing pre key bundle for: %@.", recipientID);
} else {
SessionBuilder *builder = [[SessionBuilder alloc] initWithSessionStore:storage
preKeyStore:storage
signedPreKeyStore:storage
identityKeyStore:self.identityManager
recipientId:recipientID
deviceId:[deviceId intValue]];
[LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
@try {
[builder throws_processPrekeyBundle:bundle protocolContext:transaction];
// Loki: Discard the pre key bundle as the session has now been established
[storage removePreKeyBundleForContact:recipientID transaction:transaction];
} @catch (NSException *caughtException) {
exception = caughtException;
}
} error:nil];
if (exception) {
if ([exception.name isEqualToString:UntrustedIdentityKeyException]) {
OWSRaiseExceptionWithUserInfo(UntrustedIdentityKeyException, (@{ TSInvalidPreKeyBundleKey : bundle, TSInvalidRecipientKey : recipientID }), @"");
}
@throw exception;
}
return YES;
}
}
- (nullable NSDictionary *)throws_encryptedMessageForMessageSend:(OWSMessageSend *)messageSend
recipientID:(NSString *)recipientID
plainText:(NSData *)plainText
transaction:(YapDatabaseReadWriteTransaction *)transaction
{
OWSAssertDebug(messageSend);
OWSAssertDebug(recipientID);
OWSAssertDebug(plainText);
OWSAssertDebug(transaction);
OWSPrimaryStorage *storage = self.primaryStorage;
TSOutgoingMessage *message = messageSend.message;
SessionCipher *cipher = [[SessionCipher alloc] initWithSessionStore:storage
preKeyStore:storage
signedPreKeyStore:storage
identityKeyStore:self.identityManager
recipientId:recipientID
deviceId:@(OWSDevicePrimaryDeviceId).intValue];
NSData *_Nullable serializedMessage;
TSWhisperMessageType messageType;
if ([LKSharedSenderKeysImplementation.shared isClosedGroup:recipientID]) {
NSError *error;
serializedMessage = [LKClosedGroupUtilities encryptData:plainText.paddedMessageBody usingGroupPublicKey:recipientID transaction:transaction error:&error];
if (error != nil) {
OWSFailDebug(@"Couldn't encrypt message for SSK based closed group due to error: %@.", error);
return nil;
}
messageType = TSClosedGroupCiphertextMessageType;
messageSend.udAccess = nil;
} else if (messageSend.isUDSend) {
NSError *error;
LKSessionResetImplementation *sessionResetImplementation = [LKSessionResetImplementation new];
SMKSecretSessionCipher *_Nullable secretCipher =
[[SMKSecretSessionCipher alloc] initWithSessionResetImplementation:sessionResetImplementation
sessionStore:self.primaryStorage
preKeyStore:self.primaryStorage
signedPreKeyStore:self.primaryStorage
identityStore:self.identityManager
error:&error];
if (error || !secretCipher) {
OWSRaiseException(@"SecretSessionCipherFailure", @"Can't create secret session cipher.");
}
// Loki: The way this works is:
// • Alice sends a session request (i.e. a pre key bundle) to Bob using fallback encryption.
// • She may send any number of subsequent messages also encrypted using fallback encryption.
// • When Bob receives the session request, he sets up his Signal cipher session locally and sends back a null message,
// now encrypted using Signal encryption.
// • Alice receives this, sets up her Signal cipher session locally, and sends any subsequent messages
// using Signal encryption.
BOOL shouldUseFallbackEncryption = [LKSessionManagementProtocol shouldUseFallbackEncryptionForMessage:message recipientID:recipientID transaction:transaction];
if (shouldUseFallbackEncryption) {
[LKLogger print:@"[Loki] Using fallback encryption"];
} else {
[LKLogger print:@"[Loki] Using Signal Encryption"];
}
serializedMessage = [secretCipher throwswrapped_encryptMessageWithRecipientPublicKey:recipientID
deviceID:@(OWSDevicePrimaryDeviceId).intValue
paddedPlaintext:plainText.paddedMessageBody
senderCertificate:messageSend.senderCertificate
protocolContext:transaction
useFallbackSessionCipher:shouldUseFallbackEncryption
error:&error];
SCKRaiseIfExceptionWrapperError(error);
if (serializedMessage == nil || error != nil) {
OWSFailDebug(@"Error while UD encrypting message: %@.", error);
return nil;
}
messageType = TSUnidentifiedSenderMessageType;
} else {
id<CipherMessage> encryptedMessage =
[cipher throws_encryptMessage:[plainText paddedMessageBody] protocolContext:transaction];
serializedMessage = encryptedMessage.serialized;
messageType = [self messageTypeForCipherMessage:encryptedMessage];
}
BOOL isSilent = message.isSilent;
BOOL isOnline = message.isOnline;
OWSMessageServiceParams *messageParams =
[[OWSMessageServiceParams alloc] initWithType:messageType
recipientId:recipientID
device:@(OWSDevicePrimaryDeviceId).intValue
content:serializedMessage
isSilent:isSilent
isOnline:isOnline
registrationId:[cipher throws_remoteRegistrationId:transaction]
ttl:message.ttl
isPing:NO];
NSError *error;
NSDictionary *jsonDict = [MTLJSONAdapter JSONDictionaryFromModel:messageParams error:&error];
if (error != nil) {
OWSProdError([OWSAnalyticsEvents messageSendErrorCouldNotSerializeMessageJson]);
return nil;
}
return jsonDict;
}
- (TSWhisperMessageType)messageTypeForCipherMessage:(id<CipherMessage>)cipherMessage
{
switch (cipherMessage.cipherMessageType) {
case CipherMessageType_Whisper:
return TSEncryptedWhisperMessageType;
case CipherMessageType_Prekey:
return TSPreKeyWhisperMessageType;
default:
return TSUnknownMessageType;
}
}
- (void)saveInfoMessageForGroupMessage:(TSOutgoingMessage *)message inThread:(TSThread *)thread
{
OWSAssertDebug(message);
OWSAssertDebug(thread);
if (message.groupMetaMessage == TSGroupMetaMessageDeliver) {
// TODO: Why is this necessary?
[message save];
} else if (message.groupMetaMessage == TSGroupMetaMessageQuit) {
// MJK TODO - remove senderTimestamp
[[[TSInfoMessage alloc] initWithTimestamp:message.timestamp
inThread:thread
messageType:TSInfoMessageTypeGroupQuit
customMessage:message.customMessage] save];
} else {
// MJK TODO - remove senderTimestamp
[[[TSInfoMessage alloc] initWithTimestamp:message.timestamp
inThread:thread
messageType:TSInfoMessageTypeGroupUpdate
customMessage:message.customMessage] save];
}
}
@end
@implementation OutgoingMessagePreparer
#pragma mark - Dependencies
+ (YapDatabaseConnection *)dbConnection
{
return SSKEnvironment.shared.primaryStorage.dbReadWriteConnection;
}
#pragma mark -
+ (NSArray<NSString *> *)prepareMessageForSending:(TSOutgoingMessage *)message
transaction:(YapDatabaseReadWriteTransaction *)transaction
{
OWSAssertDebug(message);
OWSAssertDebug(transaction);
NSMutableArray<NSString *> *attachmentIds = [NSMutableArray new];
if (message.attachmentIds) {
[attachmentIds addObjectsFromArray:message.attachmentIds];
}
if (message.quotedMessage) {
// Though we currently only ever expect at most one thumbnail, the proto data model
// suggests this could change. The logic is intended to work with multiple, but
// if we ever actually want to send multiple, we should do more testing.
NSArray<TSAttachmentStream *> *quotedThumbnailAttachments =
[message.quotedMessage createThumbnailAttachmentsIfNecessaryWithTransaction:transaction];
for (TSAttachmentStream *attachment in quotedThumbnailAttachments) {
[attachmentIds addObject:attachment.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) {
TSAttachment *attachment =
[TSAttachment fetchObjectWithUniqueID:message.linkPreview.imageAttachmentId transaction:transaction];
if ([attachment isKindOfClass:[TSAttachmentStream class]]) {
[attachmentIds addObject:attachment.uniqueId];
} else {
OWSFailDebug(@"Unexpected attachment: %@.", attachment);
}
}
// All outgoing messages should be saved at the time they are enqueued.
[message saveWithTransaction:transaction];
// When we start a message send, all "failed" recipients should be marked as "sending".
[message updateWithMarkingAllUnsentRecipientsAsSendingWithTransaction:transaction];
return attachmentIds;
}
+ (void)prepareAttachments:(NSArray<OWSOutgoingAttachmentInfo *> *)attachmentInfos
inMessage:(TSOutgoingMessage *)outgoingMessage
completionHandler:(void (^)(NSError *_Nullable error))completionHandler
{
OWSAssertDebug(attachmentInfos.count > 0);
OWSAssertDebug(outgoingMessage);
dispatch_async([OWSDispatch attachmentsQueue], ^{
NSMutableArray<TSAttachmentStream *> *attachmentStreams = [NSMutableArray new];
for (OWSOutgoingAttachmentInfo *attachmentInfo in attachmentInfos) {
TSAttachmentStream *attachmentStream =
[[TSAttachmentStream alloc] initWithContentType:attachmentInfo.contentType
byteCount:(UInt32)attachmentInfo.dataSource.dataLength
sourceFilename:attachmentInfo.sourceFilename
caption:attachmentInfo.caption
albumMessageId:attachmentInfo.albumMessageId];
if (outgoingMessage.isVoiceMessage) {
attachmentStream.attachmentType = TSAttachmentTypeVoiceMessage;
}
if (![attachmentStream writeDataSource:attachmentInfo.dataSource]) {
OWSProdError([OWSAnalyticsEvents messageSenderErrorCouldNotWriteAttachment]);
NSError *error = OWSErrorMakeWriteAttachmentDataError();
completionHandler(error);
return;
}
[attachmentStreams addObject:attachmentStream];
}
[LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *_Nonnull transaction) {
for (TSAttachmentStream *attachmentStream in attachmentStreams) {
[outgoingMessage.attachmentIds addObject:attachmentStream.uniqueId];
if (attachmentStream.sourceFilename) {
outgoingMessage.attachmentFilenameMap[attachmentStream.uniqueId] = attachmentStream.sourceFilename;
}
}
[outgoingMessage saveWithTransaction:transaction];
for (TSAttachmentStream *attachmentStream in attachmentStreams) {
[attachmentStream saveWithTransaction:transaction];
}
} error:nil];
completionHandler(nil);
});
}
@end
NS_ASSUME_NONNULL_END