2017-01-23 00:09:38 +01:00
//
// Copyright ( c ) 2017 Open Whisper Systems . All rights reserved .
//
2016-10-08 01:17:38 +02:00
# import "OWSMessageSender.h"
2016-10-14 23:00:29 +02:00
# import "ContactsUpdater.h"
2017-06-08 05:21:25 +02:00
# import "NSData+keyVersionByte.h"
2016-10-14 23:00:29 +02:00
# import "NSData+messagePadding.h"
2017-04-03 20:42:04 +02:00
# import "OWSBlockingManager.h"
2017-02-16 00:32:27 +01:00
# import "OWSDevice.h"
2016-10-14 23:00:29 +02:00
# import "OWSDisappearingMessagesJob.h"
2016-10-08 01:17:38 +02:00
# import "OWSError.h"
2017-06-06 20:12:50 +02:00
# import "OWSIdentityManager.h"
2016-10-14 23:00:29 +02:00
# import "OWSMessageServiceParams.h"
# import "OWSOutgoingSentMessageTranscript.h"
# import "OWSOutgoingSyncMessage.h"
# import "OWSUploadingService.h"
# import "PreKeyBundle+jsonDict.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"
2016-10-08 01:17:38 +02:00
# import "TSNetworkManager.h"
# import "TSOutgoingMessage.h"
2017-02-10 19:20:11 +01:00
# import "TSPreKeyManager.h"
2016-10-14 23:00:29 +02:00
# import "TSStorageManager+PreKeyStore.h"
# import "TSStorageManager+SignedPreKeyStore.h"
# import "TSStorageManager+sessionStore.h"
# import "TSStorageManager.h"
2016-10-08 01:17:38 +02:00
# import "TSThread.h"
2017-09-21 17:55:25 +02:00
# import "Threading.h"
2016-10-14 23:00:29 +02:00
# import < AxolotlKit / AxolotlExceptions . h >
# import < AxolotlKit / CipherMessage . h >
# import < AxolotlKit / PreKeyBundle . h >
# import < AxolotlKit / SessionBuilder . h >
# import < AxolotlKit / SessionCipher . h >
# import < TwistedOakCollapsingFutures / CollapsingFutures . h >
2017-04-14 16:25:52 +02:00
# import < objc / runtime . h >
2016-10-08 01:17:38 +02:00
NS_ASSUME _NONNULL _BEGIN
2017-04-07 01:11:04 +02:00
void AssertIsOnSendingQueue ( )
{
2017-04-07 18:46:42 +02:00
# ifdef DEBUG
2017-04-07 01:11:04 +02:00
if ( SYSTEM_VERSION _GREATER _THAN _OR _EQUAL _TO ( 10 , 0 ) ) {
dispatch_assert _queue ( [ OWSDispatch sendingQueue ] ) ;
} // else , skip assert as it ' s a development convenience .
2017-04-07 18:46:42 +02:00
# endif
2017-04-07 01:11:04 +02:00
}
2017-04-14 16:25:52 +02:00
static void * kNSError_MessageSender _IsRetryable = & kNSError_MessageSender _IsRetryable ;
static void * kNSError_MessageSender _ShouldBeIgnoredForGroups = & kNSError_MessageSender _ShouldBeIgnoredForGroups ;
2017-04-19 21:39:34 +02:00
static void * kNSError_MessageSender _IsFatal = & kNSError_MessageSender _IsFatal ;
2017-04-14 16:25:52 +02:00
2017-04-19 21:39:34 +02:00
// isRetryable and isFatal are opposites but not redundant .
//
// If a group message send fails , the send will be retried if any of the errors were retryable UNLESS
// any of the errors were fatal . Fatal errors trump retryable errors .
2017-04-14 16:25:52 +02:00
@ implementation NSError ( OWSMessageSender )
- ( BOOL ) isRetryable
{
NSNumber * value = objc_getAssociatedObject ( self , kNSError_MessageSender _IsRetryable ) ;
// This value should always be set for all errors by the time OWSSendMessageOperation
// queries it ' s value . If not , default to retrying in production .
OWSAssert ( value ) ;
return value ? [ value boolValue ] : YES ;
}
- ( void ) setIsRetryable : ( BOOL ) value
{
objc_setAssociatedObject ( self , kNSError_MessageSender _IsRetryable , @ ( value ) , OBJC_ASSOCIATION _COPY ) ;
}
- ( BOOL ) shouldBeIgnoredForGroups
{
NSNumber * value = objc_getAssociatedObject ( self , kNSError_MessageSender _ShouldBeIgnoredForGroups ) ;
// This value will NOT always be set for all errors by the time we query it ' s value .
// Default to NOT ignoring .
return value ? [ value boolValue ] : NO ;
}
- ( void ) setShouldBeIgnoredForGroups : ( BOOL ) value
{
objc_setAssociatedObject ( self , kNSError_MessageSender _ShouldBeIgnoredForGroups , @ ( value ) , OBJC_ASSOCIATION _COPY ) ;
}
2017-04-19 21:39:34 +02:00
- ( BOOL ) isFatal
{
NSNumber * value = objc_getAssociatedObject ( self , kNSError_MessageSender _IsFatal ) ;
// This value will NOT always be set for all errors by the time we query it ' s value .
// Default to NOT fatal .
return value ? [ value boolValue ] : NO ;
}
- ( void ) setIsFatal : ( BOOL ) value
{
objc_setAssociatedObject ( self , kNSError_MessageSender _IsFatal , @ ( value ) , OBJC_ASSOCIATION _COPY ) ;
}
2017-04-14 16:25:52 +02:00
@ end
# pragma mark -
2017-03-17 17:26:25 +01:00
/ * *
* OWSSendMessageOperation encapsulates all the work associated with sending a message , e . g . uploading attachments ,
2017-03-20 19:57:05 +01:00
* getting proper keys , and retrying upon failure .
2017-03-17 17:26:25 +01:00
*
* Used by ` OWSMessageSender` to serialize message sending , ensuring that messages are emitted in the order they
* were sent .
* /
@ interface OWSSendMessageOperation : NSOperation
- ( instancetype ) init NS_UNAVAILABLE ;
- ( instancetype ) initWithMessage : ( TSOutgoingMessage * ) message
messageSender : ( OWSMessageSender * ) messageSender
success : ( void ( ^ ) ( ) ) successHandler
failure : ( void ( ^ ) ( NSError * _Nonnull error ) ) failureHandler NS_DESIGNATED _INITIALIZER ;
2017-03-17 20:38:22 +01:00
# pragma mark - background task mgmt
- ( void ) startBackgroundTask ;
- ( void ) endBackgroundTask ;
2017-03-17 17:26:25 +01:00
@ end
2017-04-10 16:35:43 +02:00
# pragma mark -
2017-03-17 17:26:25 +01:00
typedef NS_ENUM ( NSInteger , OWSSendMessageOperationState ) {
OWSSendMessageOperationStateNew ,
OWSSendMessageOperationStateExecuting ,
OWSSendMessageOperationStateFinished
} ;
@ interface OWSMessageSender ( OWSSendMessageOperation )
- ( void ) attemptToSendMessage : ( TSOutgoingMessage * ) message
success : ( void ( ^ ) ( ) ) successHandler
2017-04-05 01:44:14 +02:00
failure : ( RetryableFailureHandler ) failureHandler ;
2017-03-17 17:26:25 +01:00
@ end
2017-04-10 16:35:43 +02:00
# pragma mark -
2017-03-17 17:26:25 +01:00
NSString * const OWSSendMessageOperationKeyIsExecuting = @ "isExecuting" ;
NSString * const OWSSendMessageOperationKeyIsFinished = @ "isFinished" ;
NSUInteger const OWSSendMessageOperationMaxRetries = 4 ;
@ interface OWSSendMessageOperation ( )
@ property ( nonatomic , readonly ) TSOutgoingMessage * message ;
@ property ( nonatomic , readonly ) OWSMessageSender * messageSender ;
@ property ( nonatomic , readonly ) void ( ^ successHandler ) ( ) ;
@ property ( nonatomic , readonly ) void ( ^ failureHandler ) ( NSError * _Nonnull error ) ;
2017-03-17 20:38:22 +01:00
@ property ( nonatomic ) OWSSendMessageOperationState operationState ;
@ property ( nonatomic ) UIBackgroundTaskIdentifier backgroundTaskIdentifier ;
2017-03-17 17:26:25 +01:00
@ end
2017-04-10 16:35:43 +02:00
# pragma mark -
2017-03-17 17:26:25 +01:00
@ implementation OWSSendMessageOperation
- ( instancetype ) initWithMessage : ( TSOutgoingMessage * ) message
messageSender : ( OWSMessageSender * ) messageSender
success : ( void ( ^ ) ( ) ) aSuccessHandler
failure : ( void ( ^ ) ( NSError * _Nonnull error ) ) aFailureHandler
{
self = [ super init ] ;
if ( ! self ) {
return self ;
}
_operationState = OWSSendMessageOperationStateNew ;
2017-03-17 20:38:22 +01:00
_backgroundTaskIdentifier = UIBackgroundTaskInvalid ;
2017-03-17 17:26:25 +01:00
_message = message ;
_messageSender = messageSender ;
__weak typeof ( self ) weakSelf = self ;
_successHandler = ^ {
typeof ( self ) strongSelf = weakSelf ;
if ( ! strongSelf ) {
2017-07-27 18:29:05 +02:00
OWSProdCFail ( [ OWSAnalyticsEvents messageSenderErrorSendOperationDidNotComplete ] ) ;
2017-03-17 17:26:25 +01:00
return ;
}
2017-04-11 22:57:28 +02:00
[ message updateWithMessageState : TSOutgoingMessageStateSentToService ] ;
2017-03-17 17:26:25 +01:00
aSuccessHandler ( ) ;
2017-06-15 21:19:55 +02:00
2017-06-17 19:41:29 +02:00
[ strongSelf markAsComplete ] ;
2017-03-17 17:26:25 +01:00
} ;
_failureHandler = ^ ( NSError * _Nonnull error ) {
typeof ( self ) strongSelf = weakSelf ;
if ( ! strongSelf ) {
2017-07-27 18:29:05 +02:00
OWSProdCFail ( [ OWSAnalyticsEvents messageSenderErrorSendOperationDidNotComplete ] ) ;
2017-03-17 17:26:25 +01:00
return ;
}
2017-04-11 22:57:28 +02:00
[ strongSelf . message updateWithSendingError : error ] ;
2017-04-10 18:35:30 +02:00
2017-03-17 17:26:25 +01:00
DDLogDebug ( @ "%@ failed with error: %@" , strongSelf . tag , error ) ;
aFailureHandler ( error ) ;
2017-06-15 21:19:55 +02:00
2017-06-17 19:41:29 +02:00
[ strongSelf markAsComplete ] ;
2017-03-17 17:26:25 +01:00
} ;
return self ;
}
2017-03-17 20:38:22 +01:00
# pragma mark - background task mgmt
// We want to make sure to finish sending any in - flight messages when the app is backgrounded .
// We have to call ` startBackgroundTask` * before * the task is enqueued , since we can ' t guarantee when the operation will
// be dequeued .
- ( void ) startBackgroundTask
{
AssertIsOnMainThread ( ) ;
OWSAssert ( self . backgroundTaskIdentifier = = UIBackgroundTaskInvalid ) ;
self . backgroundTaskIdentifier = [ [ UIApplication sharedApplication ] beginBackgroundTaskWithExpirationHandler : ^ {
DDLogWarn ( @ "%@ Timed out while in background trying to send message: %@" , self . tag , self . message ) ;
[ self endBackgroundTask ] ;
} ] ;
}
- ( void ) endBackgroundTask
{
[ [ UIApplication sharedApplication ] endBackgroundTask : self . backgroundTaskIdentifier ] ;
}
- ( void ) setBackgroundTaskIdentifier : ( UIBackgroundTaskIdentifier ) backgroundTaskIdentifier
{
AssertIsOnMainThread ( ) ;
// Should only be sent once per operation
OWSAssert ( _backgroundTaskIdentifier = = UIBackgroundTaskInvalid ) ;
OWSAssert ( backgroundTaskIdentifier ! = UIBackgroundTaskInvalid ) ;
_backgroundTaskIdentifier = backgroundTaskIdentifier ;
}
2017-03-17 17:26:25 +01:00
# pragma mark - NSOperation overrides
- ( BOOL ) isExecuting
{
return self . operationState = = OWSSendMessageOperationStateExecuting ;
}
- ( BOOL ) isFinished
{
return self . operationState = = OWSSendMessageOperationStateFinished ;
}
- ( void ) start
{
2017-03-17 20:38:22 +01:00
// Should call ` startBackgroundTask` before enqueuing the operation
// to ensure we don ' t get suspended before the operation completes .
OWSAssert ( self . backgroundTaskIdentifier ! = UIBackgroundTaskInvalid ) ;
2017-03-17 17:26:25 +01:00
[ self willChangeValueForKey : OWSSendMessageOperationKeyIsExecuting ] ;
self . operationState = OWSSendMessageOperationStateExecuting ;
[ self didChangeValueForKey : OWSSendMessageOperationKeyIsExecuting ] ;
[ self main ] ;
}
- ( void ) main
{
[ self tryWithRemainingRetries : OWSSendMessageOperationMaxRetries ] ;
}
# pragma mark - methods
- ( void ) tryWithRemainingRetries : ( NSUInteger ) remainingRetries
{
2017-06-13 21:08:27 +02:00
// Use this flag to ensure a given operation only succeeds or fails once .
__block BOOL onceFlag = NO ;
2017-04-14 16:25:52 +02:00
RetryableFailureHandler retryableFailureHandler = ^ ( NSError * _Nonnull error ) {
2017-07-10 18:52:22 +02:00
DDLogInfo ( @ "%@ Sending failed. Remaining retries: %lu" , self . tag , ( unsigned long ) remainingRetries ) ;
2017-04-05 01:44:14 +02:00
2017-06-13 21:08:27 +02:00
OWSAssert ( ! onceFlag ) ;
onceFlag = YES ;
2017-04-19 21:39:34 +02:00
if ( ! [ error isRetryable ] || [ error isFatal ] ) {
2017-04-05 01:44:14 +02:00
DDLogInfo ( @ "%@ Skipping retry due to terminal error: %@" , self . tag , error ) ;
self . failureHandler ( error ) ;
return ;
}
2017-03-17 17:26:25 +01:00
if ( remainingRetries > 0 ) {
[ self tryWithRemainingRetries : remainingRetries - 1 ] ;
} else {
DDLogWarn ( @ "%@ Too many failures. Giving up sending." , self . tag ) ;
2017-04-10 16:35:43 +02:00
2017-03-17 17:26:25 +01:00
self . failureHandler ( error ) ;
}
} ;
2017-06-13 21:08:27 +02:00
[ self . messageSender attemptToSendMessage : self . message
success : ^ {
OWSAssert ( ! onceFlag ) ;
onceFlag = YES ;
self . successHandler ( ) ;
}
failure : retryableFailureHandler ] ;
2017-03-17 17:26:25 +01:00
}
- ( void ) markAsComplete
{
[ self willChangeValueForKey : OWSSendMessageOperationKeyIsExecuting ] ;
[ self willChangeValueForKey : OWSSendMessageOperationKeyIsFinished ] ;
2017-06-17 19:41:29 +02:00
// Ensure we call the success or failure handler exactly once .
@ synchronized ( self )
{
OWSAssert ( self . operationState ! = OWSSendMessageOperationStateFinished ) ;
self . operationState = OWSSendMessageOperationStateFinished ;
}
2017-03-17 17:26:25 +01:00
[ self didChangeValueForKey : OWSSendMessageOperationKeyIsExecuting ] ;
[ self didChangeValueForKey : OWSSendMessageOperationKeyIsFinished ] ;
2017-03-17 20:38:22 +01:00
[ self endBackgroundTask ] ;
2017-03-17 17:26:25 +01:00
}
# pragma mark - Logging
+ ( NSString * ) tag
{
return [ NSString stringWithFormat : @ "[%@]" , self . class ] ;
}
- ( NSString * ) tag
{
return self . class . tag ;
}
@ end
2016-10-14 23:00:29 +02:00
int const OWSMessageSenderRetryAttempts = 3 ;
NSString * const OWSMessageSenderInvalidDeviceException = @ "InvalidDeviceException" ;
2016-11-10 15:59:07 +01:00
NSString * const OWSMessageSenderRateLimitedException = @ "RateLimitedException" ;
2016-10-14 23:00:29 +02:00
2016-10-08 01:17:38 +02:00
@ interface OWSMessageSender ( )
@ property ( nonatomic , readonly ) TSNetworkManager * networkManager ;
2016-10-14 23:00:29 +02:00
@ property ( nonatomic , readonly ) TSStorageManager * storageManager ;
2017-04-03 20:42:04 +02:00
@ property ( nonatomic , readonly ) OWSBlockingManager * blockingManager ;
2016-10-14 23:00:29 +02:00
@ property ( nonatomic , readonly ) OWSUploadingService * uploadingService ;
@ property ( nonatomic , readonly ) YapDatabaseConnection * dbConnection ;
@ property ( nonatomic , readonly ) id < ContactsManagerProtocol > contactsManager ;
@ property ( nonatomic , readonly ) ContactsUpdater * contactsUpdater ;
2017-03-23 19:35:30 +01:00
@ property ( atomic , readonly ) NSMutableDictionary < NSString * , NSOperationQueue * > * sendingQueueMap ;
2016-10-08 01:17:38 +02:00
@ end
@ implementation OWSMessageSender
2016-10-14 23:00:29 +02:00
- ( instancetype ) initWithNetworkManager : ( TSNetworkManager * ) networkManager
storageManager : ( TSStorageManager * ) storageManager
contactsManager : ( id < ContactsManagerProtocol > ) contactsManager
contactsUpdater : ( ContactsUpdater * ) contactsUpdater
2016-10-08 01:17:38 +02:00
{
self = [ super init ] ;
if ( ! self ) {
return self ;
}
_networkManager = networkManager ;
2016-10-14 23:00:29 +02:00
_storageManager = storageManager ;
_contactsManager = contactsManager ;
_contactsUpdater = contactsUpdater ;
2017-03-23 17:41:15 +01:00
_sendingQueueMap = [ NSMutableDictionary new ] ;
2016-10-14 23:00:29 +02:00
_uploadingService = [ [ OWSUploadingService alloc ] initWithNetworkManager : networkManager ] ;
_dbConnection = storageManager . newDatabaseConnection ;
2017-04-01 00:45:46 +02:00
OWSSingletonAssert ( ) ;
2016-10-08 01:17:38 +02:00
return self ;
}
2017-04-03 20:42:04 +02:00
- ( void ) setBlockingManager : ( OWSBlockingManager * ) blockingManager
2017-04-01 22:47:16 +02:00
{
OWSAssert ( blockingManager ) ;
OWSAssert ( ! _blockingManager ) ;
_blockingManager = blockingManager ;
}
2017-03-23 17:41:15 +01:00
- ( NSOperationQueue * ) sendingQueueForMessage : ( TSOutgoingMessage * ) message
{
OWSAssert ( message ) ;
NSString * kDefaultQueueKey = @ "kDefaultQueueKey" ;
NSString * queueKey = message . uniqueThreadId ? : kDefaultQueueKey ;
OWSAssert ( queueKey . length > 0 ) ;
@ synchronized ( self )
{
NSOperationQueue * sendingQueue = self . sendingQueueMap [ queueKey ] ;
if ( ! sendingQueue ) {
sendingQueue = [ NSOperationQueue new ] ;
sendingQueue . qualityOfService = NSOperationQualityOfServiceUserInitiated ;
sendingQueue . maxConcurrentOperationCount = 1 ;
self . sendingQueueMap [ queueKey ] = sendingQueue ;
}
return sendingQueue ;
}
}
2016-10-14 23:00:29 +02:00
- ( void ) sendMessage : ( TSOutgoingMessage * ) message
success : ( void ( ^ ) ( ) ) successHandler
failure : ( void ( ^ ) ( NSError * error ) ) failureHandler
2017-03-17 17:26:25 +01:00
{
2017-04-19 21:17:56 +02:00
OWSAssert ( message ) ;
2017-03-20 20:23:41 +01:00
2017-09-21 17:55:25 +02:00
dispatch_async ( dispatch_get _global _queue ( DISPATCH_QUEUE _PRIORITY _DEFAULT , 0 ) , ^ {
// 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
2017-09-21 23:25:13 +02:00
// 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 db .
//
// So we ' re using YDB behavior to ensure this invariant , which is a bit
// unorthodox .
2017-09-13 21:35:33 +02:00
[ message updateWithMessageState : TSOutgoingMessageStateAttemptingOut ] ;
2017-03-17 20:38:22 +01:00
2017-09-21 17:55:25 +02:00
OWSSendMessageOperation * sendMessageOperation =
[ [ OWSSendMessageOperation alloc ] initWithMessage : message
messageSender : self
success : successHandler
failure : failureHandler ] ;
2017-03-23 17:41:15 +01:00
2017-09-21 17:55:25 +02:00
// startBackgroundTask must be called on the main thread .
dispatch_async ( dispatch_get _main _queue ( ) , ^ {
// We call ` startBackgroundTask` here to prevent our app from suspending while being backgrounded
// until the operation is completed - at which point the OWSSendMessageOperation ends it ' s background task .
[ sendMessageOperation startBackgroundTask ] ;
dispatch_async ( dispatch_get _global _queue ( DISPATCH_QUEUE _PRIORITY _DEFAULT , 0 ) , ^ {
NSOperationQueue * sendingQueue = [ self sendingQueueForMessage : message ] ;
[ sendingQueue addOperation : sendMessageOperation ] ;
} ) ;
} ) ;
} ) ;
2017-03-17 17:26:25 +01:00
}
- ( void ) attemptToSendMessage : ( TSOutgoingMessage * ) message
success : ( void ( ^ ) ( ) ) successHandler
2017-04-05 01:44:14 +02:00
failure : ( RetryableFailureHandler ) failureHandler
2016-10-14 23:00:29 +02:00
{
[ self ensureAnyAttachmentsUploaded : message
2017-04-10 16:35:43 +02:00
success : ^ ( ) {
[ self deliverMessage : message
success : successHandler
2017-04-14 16:25:52 +02:00
failure : ^ ( NSError * error ) {
2017-04-10 16:35:43 +02:00
DDLogDebug ( @ "%@ Message send attempt failed: %@" , self . tag , message . debugDescription ) ;
2017-04-14 16:25:52 +02:00
failureHandler ( error ) ;
2017-04-10 16:35:43 +02:00
} ] ;
}
2017-04-14 16:25:52 +02:00
failure : ^ ( NSError * error ) {
2017-04-10 16:35:43 +02:00
DDLogDebug ( @ "%@ Attachment upload attempt failed: %@" , self . tag , message . debugDescription ) ;
2017-04-14 16:25:52 +02:00
failureHandler ( error ) ;
2017-04-10 16:35:43 +02:00
} ] ;
2016-10-14 23:00:29 +02:00
}
- ( void ) ensureAnyAttachmentsUploaded : ( TSOutgoingMessage * ) message
success : ( void ( ^ ) ( ) ) successHandler
2017-04-05 01:44:14 +02:00
failure : ( RetryableFailureHandler ) failureHandler
2016-10-14 23:00:29 +02:00
{
if ( ! message . hasAttachments ) {
return successHandler ( ) ;
}
TSAttachmentStream * attachmentStream =
[ TSAttachmentStream fetchObjectWithUniqueID : message . attachmentIds . firstObject ] ;
if ( ! attachmentStream ) {
2017-07-27 18:29:05 +02:00
OWSProdError ( [ OWSAnalyticsEvents messageSenderErrorCouldNotLoadAttachment ] ) ;
2016-10-14 23:00:29 +02:00
NSError * error = OWSErrorMakeFailedToSendOutgoingMessageError ( ) ;
2017-04-05 01:44:14 +02:00
// Not finding local attachment is a terminal failure .
2017-04-14 16:25:52 +02:00
[ error setIsRetryable : NO ] ;
return failureHandler ( error ) ;
2016-10-14 23:00:29 +02:00
}
[ self . uploadingService uploadAttachmentStream : attachmentStream
message : message
success : successHandler
failure : failureHandler ] ;
}
2017-09-18 21:02:34 +02:00
- ( void ) sendTemporaryAttachmentData : ( DataSource * ) dataSource
2016-10-14 23:00:29 +02:00
contentType : ( NSString * ) contentType
inMessage : ( TSOutgoingMessage * ) message
success : ( void ( ^ ) ( ) ) successHandler
failure : ( void ( ^ ) ( NSError * error ) ) failureHandler
{
2017-09-08 18:51:25 +02:00
OWSAssert ( dataSource ) ;
2016-10-14 23:00:29 +02:00
void ( ^ successWithDeleteHandler ) ( ) = ^ ( ) {
successHandler ( ) ;
DDLogDebug ( @ "Removing temporary attachment message." ) ;
[ message remove ] ;
} ;
void ( ^ failureWithDeleteHandler ) ( NSError * error ) = ^ ( NSError * error ) {
failureHandler ( error ) ;
DDLogDebug ( @ "Removing temporary attachment message." ) ;
[ message remove ] ;
} ;
2017-09-08 18:51:25 +02:00
[ self sendAttachmentData : dataSource
2016-10-14 23:00:29 +02:00
contentType : contentType
2017-05-15 15:53:16 +02:00
sourceFilename : nil
2016-10-14 23:00:29 +02:00
inMessage : message
success : successWithDeleteHandler
failure : failureWithDeleteHandler ] ;
}
2017-09-18 21:02:34 +02:00
- ( void ) sendAttachmentData : ( DataSource * ) dataSource
2016-10-14 23:00:29 +02:00
contentType : ( NSString * ) contentType
2017-05-15 15:53:16 +02:00
sourceFilename : ( nullable NSString * ) sourceFilename
2016-10-14 23:00:29 +02:00
inMessage : ( TSOutgoingMessage * ) message
success : ( void ( ^ ) ( ) ) successHandler
failure : ( void ( ^ ) ( NSError * error ) ) failureHandler
{
2017-09-08 18:51:25 +02:00
OWSAssert ( dataSource ) ;
2017-03-24 19:37:24 +01:00
2016-10-14 23:00:29 +02:00
dispatch_async ( [ OWSDispatch attachmentsQueue ] , ^ {
2017-10-27 00:08:25 +02:00
TSAttachmentStream * attachmentStream = [ [ TSAttachmentStream alloc ] initWithContentType : contentType
byteCount : dataSource . dataLength
sourceFilename : sourceFilename ] ;
2017-05-12 15:11:43 +02:00
if ( message . isVoiceMessage ) {
attachmentStream . attachmentType = TSAttachmentTypeVoiceMessage ;
}
2016-10-14 23:00:29 +02:00
2017-09-08 18:51:25 +02:00
if ( ! [ attachmentStream writeDataSource : dataSource ] ) {
2017-07-27 18:29:05 +02:00
OWSProdError ( [ OWSAnalyticsEvents messageSenderErrorCouldNotWriteAttachment ] ) ;
2017-09-08 18:51:25 +02:00
NSError * error = OWSErrorMakeWriteAttachmentDataError ( ) ;
2016-10-14 23:00:29 +02:00
return failureHandler ( error ) ;
}
[ attachmentStream save ] ;
[ message . attachmentIds addObject : attachmentStream . uniqueId ] ;
2017-05-15 15:53:16 +02:00
if ( sourceFilename ) {
message . attachmentFilenameMap [ attachmentStream . uniqueId ] = sourceFilename ;
2017-04-13 18:54:03 +02:00
}
2016-10-14 23:00:29 +02:00
[ message save ] ;
2017-09-21 17:55:25 +02:00
[ self sendMessage : message success : successHandler failure : failureHandler ] ;
2016-10-14 23:00:29 +02:00
} ) ;
}
- ( NSArray < SignalRecipient * > * ) getRecipients : ( NSArray < NSString * > * ) identifiers error : ( NSError * * ) error
{
NSMutableArray < SignalRecipient * > * recipients = [ NSMutableArray new ] ;
for ( NSString * recipientId in identifiers ) {
SignalRecipient * existingRecipient = [ SignalRecipient recipientWithTextSecureIdentifier : recipientId ] ;
if ( existingRecipient ) {
[ recipients addObject : existingRecipient ] ;
} else {
SignalRecipient * newRecipient = [ self . contactsUpdater synchronousLookup : recipientId error : error ] ;
if ( newRecipient ) {
[ recipients addObject : newRecipient ] ;
}
}
}
if ( recipients . count = = 0 && ! * error ) {
// error should be set in contactsUpater , but just in case .
2017-07-27 18:29:05 +02:00
OWSProdError ( [ OWSAnalyticsEvents messageSenderErrorCouldNotFindContacts1 ] ) ;
2016-10-14 23:00:29 +02:00
* error = OWSErrorMakeFailedToSendOutgoingMessageError ( ) ;
}
return [ recipients copy ] ;
}
- ( void ) deliverMessage : ( TSOutgoingMessage * ) message
success : ( void ( ^ ) ( ) ) successHandler
2017-04-05 01:44:14 +02:00
failure : ( RetryableFailureHandler ) failureHandler
2016-10-14 23:00:29 +02:00
{
dispatch_async ( [ OWSDispatch sendingQueue ] , ^ {
2017-09-29 03:12:02 +02:00
TSThread * thread = message . thread ;
2016-10-14 23:00:29 +02:00
if ( [ thread isKindOfClass : [ TSGroupThread class ] ] ) {
TSGroupThread * gThread = ( TSGroupThread * ) thread ;
NSError * error ;
NSArray < SignalRecipient * > * recipients =
[ self getRecipients : gThread . groupModel . groupMemberIds error : & error ] ;
2016-11-08 19:57:05 +01:00
if ( recipients . count = = 0 ) {
2017-04-14 16:25:52 +02:00
if ( ! error ) {
2017-07-27 18:29:05 +02:00
OWSProdError ( [ OWSAnalyticsEvents messageSenderErrorCouldNotFindContacts2 ] ) ;
2017-04-14 16:25:52 +02:00
error = OWSErrorMakeFailedToSendOutgoingMessageError ( ) ;
2016-11-08 19:57:05 +01:00
}
2017-04-17 22:45:22 +02:00
// If no recipients were found , there ' s no reason to retry . It will just fail again .
2017-04-14 16:25:52 +02:00
[ error setIsRetryable : NO ] ;
failureHandler ( error ) ;
return ;
2016-10-14 23:00:29 +02:00
}
[ self groupSend : recipients message : message thread : gThread success : successHandler failure : failureHandler ] ;
} else if ( [ thread isKindOfClass : [ TSContactThread class ] ]
|| [ message isKindOfClass : [ OWSOutgoingSyncMessage class ] ] ) {
TSContactThread * contactThread = ( TSContactThread * ) thread ;
2017-08-02 21:15:31 +02:00
if ( [ contactThread . contactIdentifier isEqualToString : [ TSAccountManager localNumber ] ]
2016-10-14 23:00:29 +02:00
&& ! [ message isKindOfClass : [ OWSOutgoingSyncMessage class ] ] ) {
[ self handleSendToMyself : message ] ;
2017-03-23 16:05:49 +01:00
successHandler ( ) ;
2016-10-14 23:00:29 +02:00
return ;
}
NSString * recipientContactId = [ message isKindOfClass : [ OWSOutgoingSyncMessage class ] ]
2017-08-02 21:15:31 +02:00
? [ TSAccountManager localNumber ]
2016-10-14 23:00:29 +02:00
: contactThread . contactIdentifier ;
2017-04-14 16:25:52 +02:00
// 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 .
2017-04-01 22:47:16 +02:00
OWSAssert ( recipientContactId . length > 0 ) ;
2017-09-20 17:48:37 +02:00
if ( [ _blockingManager isRecipientIdBlocked : recipientContactId ] ) {
2017-04-17 22:45:22 +02:00
DDLogInfo ( @ "%@ skipping 1:1 send to blocked contact: %@" , self . tag , recipientContactId ) ;
2017-04-05 23:34:47 +02:00
NSError * error = OWSErrorMakeMessageSendFailedToBlockListError ( ) ;
2017-04-05 01:44:14 +02:00
// No need to retry - the user will continue to be blocked .
2017-04-14 16:25:52 +02:00
[ error setIsRetryable : NO ] ;
failureHandler ( error ) ;
2017-04-01 22:47:16 +02:00
return ;
}
2016-10-25 15:54:43 +02:00
SignalRecipient * recipient = [ SignalRecipient recipientWithTextSecureIdentifier : recipientContactId ] ;
2016-10-14 23:00:29 +02:00
if ( ! recipient ) {
NSError * error ;
// possibly returns nil .
2016-12-16 16:26:15 +01:00
recipient = [ self . contactsUpdater synchronousLookup : recipientContactId error : & error ] ;
2016-10-14 23:00:29 +02:00
if ( error ) {
2017-02-02 01:16:07 +01:00
if ( error . code = = OWSErrorCodeNoSuchSignalRecipient ) {
DDLogWarn ( @ "%@ recipient contact not found" , self . tag ) ;
2016-10-14 23:00:29 +02:00
[ self unregisteredRecipient : recipient message : message thread : thread ] ;
}
2017-02-02 01:16:07 +01:00
2017-07-27 18:29:05 +02:00
OWSProdError ( [ OWSAnalyticsEvents messageSenderErrorCouldNotFindContacts3 ] ) ;
2017-04-05 01:44:14 +02:00
// No need to repeat trying to find a failure . Apart from repeatedly failing , it would also cause us
// to print redundant error messages .
2017-04-14 16:25:52 +02:00
[ error setIsRetryable : NO ] ;
failureHandler ( error ) ;
2017-04-01 22:47:16 +02:00
return ;
2016-10-14 23:00:29 +02:00
}
}
if ( ! recipient ) {
NSError * error = OWSErrorMakeFailedToSendOutgoingMessageError ( ) ;
DDLogWarn ( @ "recipient contact still not found after attempting lookup." ) ;
2017-04-05 01:44:14 +02:00
// No need to repeat trying to find a failure . Apart from repeatedly failing , it would also cause us to
// print redundant error messages .
2017-04-14 16:25:52 +02:00
[ error setIsRetryable : NO ] ;
failureHandler ( error ) ;
2017-04-01 22:47:16 +02:00
return ;
2016-10-14 23:00:29 +02:00
}
[ self sendMessage : message
recipient : recipient
thread : thread
attempts : OWSMessageSenderRetryAttempts
success : successHandler
failure : failureHandler ] ;
2016-11-04 17:19:13 +01:00
} else {
2017-04-05 01:44:14 +02:00
// Neither a group nor contact thread ? This should never happen .
2017-07-21 22:22:07 +02:00
OWSFail ( @ "%@ Unknown message type: %@" , self . tag , NSStringFromClass ( [ message class ] ) ) ;
2017-04-05 01:44:14 +02:00
2016-11-04 17:19:13 +01:00
NSError * error = OWSErrorMakeFailedToSendOutgoingMessageError ( ) ;
2017-04-14 16:25:52 +02:00
[ error setIsRetryable : NO ] ;
failureHandler ( error ) ;
2016-10-14 23:00:29 +02:00
}
} ) ;
}
2017-04-14 16:25:52 +02:00
// For group sends , we ' re using chained futures to make the code more readable .
2016-10-14 23:00:29 +02:00
- ( TOCFuture * ) sendMessageFuture : ( TSOutgoingMessage * ) message
recipient : ( SignalRecipient * ) recipient
thread : ( TSThread * ) thread
{
TOCFutureSource * futureSource = [ [ TOCFutureSource alloc ] init ] ;
[ self sendMessage : message
recipient : recipient
thread : thread
attempts : OWSMessageSenderRetryAttempts
success : ^ {
2017-05-02 17:31:29 +02:00
DDLogInfo ( @ "%@ Marking group message as sent to recipient: %@" , self . tag , recipient . uniqueId ) ;
2017-04-11 22:57:28 +02:00
[ message updateWithSentRecipient : recipient . uniqueId ] ;
2016-10-14 23:00:29 +02:00
[ futureSource trySetResult : @ 1 ] ;
}
2017-04-14 16:25:52 +02:00
failure : ^ ( NSError * error ) {
2016-10-14 23:00:29 +02:00
[ futureSource trySetFailure : error ] ;
} ] ;
return futureSource . future ;
}
- ( void ) groupSend : ( NSArray < SignalRecipient * > * ) recipients
message : ( TSOutgoingMessage * ) message
thread : ( TSThread * ) thread
success : ( void ( ^ ) ( ) ) successHandler
2017-04-05 01:44:14 +02:00
failure : ( RetryableFailureHandler ) failureHandler
2016-10-14 23:00:29 +02:00
{
[ self saveGroupMessage : message inThread : thread ] ;
NSMutableArray < TOCFuture * > * futures = [ NSMutableArray array ] ;
2017-04-11 22:57:28 +02:00
for ( SignalRecipient * recipient in recipients ) {
2017-05-12 17:38:24 +02:00
NSString * recipientId = recipient . recipientId ;
2017-04-01 22:47:16 +02:00
// We don ' t need to send the message to ourselves . . .
2017-08-02 21:15:31 +02:00
if ( [ recipientId isEqualToString : [ TSAccountManager localNumber ] ] ) {
2017-05-12 17:38:24 +02:00
continue ;
}
// We don ' t need to sent the message to all group members if
// it has a "single group recipient" .
if ( message . singleGroupRecipient && ! [ message . singleGroupRecipient isEqualToString : recipientId ] ) {
2017-04-11 22:57:28 +02:00
continue ;
}
2017-05-12 17:38:24 +02:00
if ( [ message wasSentToRecipient : recipientId ] ) {
2017-04-11 22:57:28 +02:00
// Skip recipients we have already sent this message to ( on an
// earlier retry , perhaps ) .
2017-05-02 17:31:29 +02:00
DDLogInfo ( @ "%@ Skipping group message recipient; already sent: %@" , self . tag , recipient . uniqueId ) ;
2017-04-01 22:47:16 +02:00
continue ;
}
// . . . otherwise we send .
2017-04-11 22:57:28 +02:00
[ futures addObject : [ self sendMessageFuture : message recipient : recipient thread : thread ] ] ;
2017-04-01 22:47:16 +02:00
}
2016-10-14 23:00:29 +02:00
TOCFuture * completionFuture = futures . toc_thenAll ;
[ completionFuture thenDo : ^ ( id value ) {
successHandler ( ) ;
} ] ;
[ completionFuture catchDo : ^ ( id failure ) {
2017-04-14 16:25:52 +02:00
// failure from toc_thenAll yields an array of failed Futures , rather than the future ' s failure .
2017-04-14 17:33:13 +02:00
NSError * firstRetryableError = nil ;
NSError * firstNonRetryableError = nil ;
2016-10-24 19:54:00 +02:00
if ( [ failure isKindOfClass : [ NSArray class ] ] ) {
2017-04-12 23:40:36 +02:00
NSArray * groupSendFutures = ( NSArray * ) failure ;
for ( TOCFuture * groupSendFuture in groupSendFutures ) {
if ( groupSendFuture . hasFailed ) {
id failureResult = groupSendFuture . forceGetFailure ;
2016-10-24 19:54:00 +02:00
if ( [ failureResult isKindOfClass : [ NSError class ] ] ) {
2017-04-14 16:25:52 +02:00
NSError * error = failureResult ;
2017-04-19 21:39:34 +02:00
2017-04-14 16:25:52 +02:00
// Some errors should be ignored when sending messages
// to groups . See discussion on
// NSError ( OWSMessageSender ) category .
if ( [ error shouldBeIgnoredForGroups ] ) {
continue ;
}
2017-04-14 17:33:13 +02:00
2017-04-19 21:39:34 +02:00
// 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 ) ;
2017-06-13 21:08:27 +02:00
return ;
2017-04-19 21:39:34 +02:00
}
2017-04-14 17:33:13 +02:00
if ( [ error isRetryable ] && ! firstRetryableError ) {
firstRetryableError = error ;
} else if ( ! [ error isRetryable ] && ! firstNonRetryableError ) {
firstNonRetryableError = error ;
}
2016-10-24 19:54:00 +02:00
}
2016-10-14 23:00:29 +02:00
}
}
}
2017-04-14 17:33:13 +02:00
// If any of the group send errors are retryable , we want to retry .
// Therefore , prefer to propagate a retryable error .
if ( firstRetryableError ) {
return failureHandler ( firstRetryableError ) ;
} else if ( firstNonRetryableError ) {
return failureHandler ( firstNonRetryableError ) ;
} else {
// If we only received errors that we should ignore ,
2017-04-17 22:45:22 +02:00
// 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 ( ) ;
}
2017-04-14 17:33:13 +02:00
}
2016-10-14 23:00:29 +02:00
} ] ;
}
- ( void ) unregisteredRecipient : ( SignalRecipient * ) recipient
message : ( TSOutgoingMessage * ) message
thread : ( TSThread * ) thread
{
[ self . dbConnection asyncReadWriteWithBlock : ^ ( YapDatabaseReadWriteTransaction * transaction ) {
[ recipient removeWithTransaction : transaction ] ;
2017-03-05 14:30:04 +01:00
[ [ TSInfoMessage userNotRegisteredMessageInThread : thread ]
2016-10-14 23:00:29 +02:00
saveWithTransaction : transaction ] ;
} ] ;
}
- ( void ) sendMessage : ( TSOutgoingMessage * ) message
recipient : ( SignalRecipient * ) recipient
thread : ( TSThread * ) thread
attempts : ( int ) remainingAttempts
success : ( void ( ^ ) ( ) ) successHandler
2017-04-05 01:44:14 +02:00
failure : ( RetryableFailureHandler ) failureHandler
2016-10-14 23:00:29 +02:00
{
2017-10-12 17:06:18 +02:00
DDLogInfo ( @ "%@ attempting to send message: %@, timestamp: %llu, recipient: %@" ,
self . tag ,
message . class ,
message . timestamp ,
recipient . uniqueId ) ;
2017-04-07 01:11:04 +02:00
AssertIsOnSendingQueue ( ) ;
2017-02-09 19:50:32 +01:00
2017-02-10 19:20:11 +01:00
if ( [ TSPreKeyManager isAppLockedDueToPreKeyUpdateFailures ] ) {
2017-07-27 18:29:05 +02:00
OWSProdError ( [ OWSAnalyticsEvents messageSendErrorFailedDueToPrekeyUpdateFailures ] ) ;
2017-02-10 19:20:11 +01:00
// 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 registerPreKeysWithMode : RefreshPreKeysMode_SignedOnly
success : ^ {
DDLogInfo ( @ "%@ New prekeys registered with server." , self . tag ) ;
}
failure : ^ ( NSError * error ) {
DDLogWarn ( @ "%@ Failed to update prekeys with the server: %@" , self . tag , error ) ;
} ] ;
2017-04-14 16:25:52 +02:00
NSError * error = OWSErrorMakeMessageSendDisabledDueToPreKeyUpdateFailuresError ( ) ;
[ error setIsRetryable : YES ] ;
return failureHandler ( error ) ;
2017-02-10 19:20:11 +01:00
}
2016-10-14 23:00:29 +02:00
if ( remainingAttempts <= 0 ) {
// We should always fail with a specific error .
2017-07-27 18:29:05 +02:00
OWSProdFail ( [ OWSAnalyticsEvents messageSenderErrorGenericSendFailure ] ) ;
2017-04-05 01:44:14 +02:00
2017-04-14 16:25:52 +02:00
NSError * error = OWSErrorMakeFailedToSendOutgoingMessageError ( ) ;
[ error setIsRetryable : YES ] ;
return failureHandler ( error ) ;
2016-10-14 23:00:29 +02:00
}
remainingAttempts - = 1 ;
NSArray < NSDictionary * > * deviceMessages ;
@ try {
2017-09-27 23:13:29 +02:00
deviceMessages = [ self deviceMessages : message forRecipient : recipient ] ;
2016-10-14 23:00:29 +02:00
} @ catch ( NSException * exception ) {
deviceMessages = @ [ ] ;
2016-11-10 15:59:07 +01:00
if ( [ exception . name isEqualToString : UntrustedIdentityKeyException ] ) {
2017-06-08 05:21:25 +02:00
// 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 .
2017-07-27 18:29:05 +02:00
OWSProdInfo ( [ OWSAnalyticsEvents messageSendErrorFailedDueToUntrustedKey ] ) ;
2017-06-08 05:21:25 +02:00
2017-06-08 16:31:10 +02:00
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 = OWSErrorWithCodeDescription ( OWSErrorCodeUntrustedIdentityKey , localizedErrorDescription ) ;
2017-06-08 05:21:25 +02:00
2017-04-05 01:44:14 +02:00
// Key will continue to be unaccepted , so no need to retry . It ' ll only cause us to hit the Pre - Key request
// rate limit
2017-04-14 16:25:52 +02:00
[ error setIsRetryable : NO ] ;
2017-04-19 21:39:34 +02:00
// Avoid the "Too many failures with this contact" error rate limiting .
[ error setIsFatal : YES ] ;
2017-06-08 05:21:25 +02:00
PreKeyBundle * newKeyBundle = exception . userInfo [ TSInvalidPreKeyBundleKey ] ;
if ( ! [ newKeyBundle isKindOfClass : [ PreKeyBundle class ] ] ) {
2017-07-27 18:29:05 +02:00
OWSProdFail ( [ OWSAnalyticsEvents messageSenderErrorUnexpectedKeyBundle ] ) ;
2017-06-08 05:21:25 +02:00
failureHandler ( error ) ;
return ;
}
NSData * newIdentityKeyWithVersion = newKeyBundle . identityKey ;
2017-06-19 17:05:06 +02:00
2017-06-08 05:21:25 +02:00
if ( ! [ newIdentityKeyWithVersion isKindOfClass : [ NSData class ] ] ) {
2017-07-27 18:29:05 +02:00
OWSProdFail ( [ OWSAnalyticsEvents messageSenderErrorInvalidIdentityKeyType ] ) ;
2017-06-08 05:21:25 +02:00
failureHandler ( error ) ;
return ;
}
2017-06-19 17:05:06 +02:00
// TODO migrate to storing the full 33 byte representation of the identity key .
if ( newIdentityKeyWithVersion . length ! = kIdentityKeyLength ) {
2017-07-27 18:29:05 +02:00
OWSProdFail ( [ OWSAnalyticsEvents messageSenderErrorInvalidIdentityKeyLength ] ) ;
2017-06-08 05:21:25 +02:00
failureHandler ( error ) ;
return ;
}
2017-06-19 17:05:06 +02:00
NSData * newIdentityKey = [ newIdentityKeyWithVersion removeKeyType ] ;
2017-06-08 05:21:25 +02:00
[ [ OWSIdentityManager sharedManager ] saveRemoteIdentity : newIdentityKey recipientId : recipient . recipientId ] ;
failureHandler ( error ) ;
return ;
2016-11-10 15:59:07 +01:00
}
if ( [ exception . name isEqualToString : OWSMessageSenderRateLimitedException ] ) {
2017-02-01 16:21:50 +01:00
NSError * error = OWSErrorWithCodeDescription ( OWSErrorCodeSignalServiceRateLimited ,
2016-11-10 15:59:07 +01:00
NSLocalizedString ( @ "FAILED_SENDING_BECAUSE_RATE_LIMIT" ,
@ "action sheet header when re-sending message which failed because of too many attempts" ) ) ;
2017-04-05 01:44:14 +02:00
// We ' re already rate - limited . No need to exacerbate the problem .
2017-04-14 16:25:52 +02:00
[ error setIsRetryable : NO ] ;
2017-04-19 21:39:34 +02:00
// Avoid exacerbating the rate limiting .
[ error setIsFatal : YES ] ;
2017-04-14 16:25:52 +02:00
return failureHandler ( error ) ;
2016-11-10 15:59:07 +01:00
}
2016-10-14 23:00:29 +02:00
if ( remainingAttempts = = 0 ) {
DDLogWarn (
@ "%@ Terminal failure to build any device messages. Giving up with exception:%@" , self . tag , exception ) ;
NSError * error = OWSErrorMakeFailedToSendOutgoingMessageError ( ) ;
2017-04-05 01:44:14 +02:00
// Since we ' ve already repeatedly failed to build messages , it ' s unlikely that repeating the whole process
// will succeed .
2017-04-14 16:25:52 +02:00
[ error setIsRetryable : NO ] ;
return failureHandler ( error ) ;
2016-10-14 23:00:29 +02:00
}
}
TSSubmitMessageRequest * request = [ [ TSSubmitMessageRequest alloc ] initWithRecipient : recipient . uniqueId
messages : deviceMessages
relay : recipient . relay
timeStamp : message . timestamp ] ;
[ self . networkManager makeRequest : request
success : ^ ( NSURLSessionDataTask * task , id responseObject ) {
dispatch_async ( [ OWSDispatch sendingQueue ] , ^ {
[ recipient save ] ;
[ self handleMessageSentLocally : message ] ;
successHandler ( ) ;
} ) ;
}
failure : ^ ( NSURLSessionDataTask * task , NSError * error ) {
2017-05-19 15:39:10 +02:00
DDLogInfo ( @ "%@ sending to recipient: %@, failed with error: %@" , self . tag , recipient . uniqueId , error ) ;
2017-02-09 19:50:32 +01:00
[ DDLog flushLog ] ;
2016-10-14 23:00:29 +02:00
NSHTTPURLResponse * response = ( NSHTTPURLResponse * ) task . response ;
long statuscode = response . statusCode ;
NSData * responseData = error . userInfo [ AFNetworkingOperationFailingURLResponseDataErrorKey ] ;
void ( ^ retrySend ) ( ) = ^ void ( ) {
if ( remainingAttempts <= 0 ) {
2017-04-05 01:44:14 +02:00
// Since we ' ve already repeatedly failed to send to the messaging API ,
// it ' s unlikely that repeating the whole process will succeed .
2017-04-14 16:25:52 +02:00
[ error setIsRetryable : NO ] ;
return failureHandler ( error ) ;
2016-10-14 23:00:29 +02:00
}
2016-10-24 19:54:00 +02:00
2016-10-14 23:00:29 +02:00
dispatch_async ( [ OWSDispatch sendingQueue ] , ^ {
2017-02-09 19:50:32 +01:00
DDLogDebug ( @ "%@ Retrying: %@" , self . tag , message . debugDescription ) ;
2016-10-14 23:00:29 +02:00
[ self sendMessage : message
recipient : recipient
thread : thread
attempts : remainingAttempts
success : successHandler
failure : failureHandler ] ;
} ) ;
} ;
switch ( statuscode ) {
2016-12-15 23:09:08 +01:00
case 401 : {
DDLogWarn ( @ "%@ Unable to send due to invalid credentials. Did the user's client get de-authed by registering elsewhere?" , self . tag ) ;
NSError * error = OWSErrorWithCodeDescription ( OWSErrorCodeSignalServiceFailure , NSLocalizedString ( @ "ERROR_DESCRIPTION_SENDING_UNAUTHORIZED" , @ "Error message when attempting to send message" ) ) ;
2017-04-05 01:44:14 +02:00
// No need to retry if we ' ve been de - authed .
2017-04-14 16:25:52 +02:00
[ error setIsRetryable : NO ] ;
return failureHandler ( error ) ;
2016-12-15 23:09:08 +01:00
}
2016-10-14 23:00:29 +02:00
case 404 : {
2017-05-02 17:31:29 +02:00
DDLogWarn ( @ "%@ Unregistered recipient: %@" , self . tag , recipient . uniqueId ) ;
2016-10-14 23:00:29 +02:00
[ self unregisteredRecipient : recipient message : message thread : thread ] ;
2016-10-25 15:54:43 +02:00
NSError * error = OWSErrorMakeNoSuchSignalRecipientError ( ) ;
2017-04-05 01:44:14 +02:00
// No need to retry if the recipient is not registered .
2017-04-14 16:25:52 +02:00
[ error setIsRetryable : NO ] ;
// If one member of a group deletes their account ,
// the group should ignore errors when trying to send
// messages to this ex - member .
[ error setShouldBeIgnoredForGroups : YES ] ;
return failureHandler ( error ) ;
2016-10-14 23:00:29 +02:00
}
case 409 : {
// Mismatched devices
2017-05-02 17:31:29 +02:00
DDLogWarn ( @ "%@ Mismatch Devices for recipient: %@" , self . tag , recipient . uniqueId ) ;
2016-10-14 23:00:29 +02:00
NSError * error ;
NSDictionary * serializedResponse =
[ NSJSONSerialization JSONObjectWithData : responseData options : 0 error : & error ] ;
if ( error ) {
2017-07-27 18:29:05 +02:00
OWSProdError ( [ OWSAnalyticsEvents messageSenderErrorCouldNotParseMismatchedDevicesJson ] ) ;
2017-04-14 16:25:52 +02:00
[ error setIsRetryable : YES ] ;
return failureHandler ( error ) ;
2016-10-14 23:00:29 +02:00
}
2017-04-07 01:11:04 +02:00
[ self handleMismatchedDevices : serializedResponse recipient : recipient completion : retrySend ] ;
2016-10-14 23:00:29 +02:00
break ;
}
case 410 : {
2017-04-14 16:25:52 +02:00
// Stale devices
2017-05-02 17:31:29 +02:00
DDLogWarn ( @ "%@ Stale devices for recipient: %@" , self . tag , recipient . uniqueId ) ;
2016-10-14 23:00:29 +02:00
if ( ! responseData ) {
DDLogWarn ( @ "Stale devices but server didn't specify devices in response." ) ;
NSError * error = OWSErrorMakeUnableToProcessServerResponseError ( ) ;
2017-04-14 16:25:52 +02:00
[ error setIsRetryable : YES ] ;
return failureHandler ( error ) ;
2016-10-14 23:00:29 +02:00
}
2017-04-07 01:11:04 +02:00
[ self handleStaleDevicesWithResponse : responseData
recipientId : recipient . uniqueId
completion : retrySend ] ;
2016-10-14 23:00:29 +02:00
break ;
}
default :
retrySend ( ) ;
break ;
}
} ] ;
}
2017-04-07 01:11:04 +02:00
- ( void ) handleMismatchedDevices : ( NSDictionary * ) dictionary
recipient : ( SignalRecipient * ) recipient
completion : ( void ( ^ ) ( ) ) completionHandler
2016-10-14 23:00:29 +02:00
{
NSArray * extraDevices = [ dictionary objectForKey : @ "extraDevices" ] ;
NSArray * missingDevices = [ dictionary objectForKey : @ "missingDevices" ] ;
2017-04-07 01:11:04 +02:00
dispatch_async ( [ OWSDispatch sessionStoreQueue ] , ^ {
2017-05-02 17:31:29 +02:00
if ( extraDevices . count < 1 && missingDevices . count < 1 ) {
2017-07-27 18:29:05 +02:00
OWSProdFail ( [ OWSAnalyticsEvents messageSenderErrorNoMissingOrExtraDevices ] ) ;
2017-05-02 17:31:29 +02:00
}
2017-04-07 01:11:04 +02:00
if ( extraDevices && extraDevices . count > 0 ) {
2017-05-02 17:31:29 +02:00
DDLogInfo ( @ "%@ removing extra devices: %@" , self . tag , extraDevices ) ;
2017-04-07 01:11:04 +02:00
for ( NSNumber * extraDeviceId in extraDevices ) {
[ self . storageManager deleteSessionForContact : recipient . uniqueId deviceId : extraDeviceId . intValue ] ;
}
2016-10-14 23:00:29 +02:00
2017-04-07 01:11:04 +02:00
[ recipient removeDevices : [ NSSet setWithArray : extraDevices ] ] ;
}
2016-10-14 23:00:29 +02:00
2017-04-07 01:11:04 +02:00
if ( missingDevices && missingDevices . count > 0 ) {
2017-05-02 17:31:29 +02:00
DDLogInfo ( @ "%@ Adding missing devices: %@" , self . tag , missingDevices ) ;
2017-04-07 01:11:04 +02:00
[ recipient addDevices : [ NSSet setWithArray : missingDevices ] ] ;
}
2016-10-14 23:00:29 +02:00
2017-04-07 01:11:04 +02:00
[ recipient save ] ;
completionHandler ( ) ;
} ) ;
2016-10-14 23:00:29 +02:00
}
- ( void ) handleMessageSentLocally : ( TSOutgoingMessage * ) message
{
if ( message . shouldSyncTranscript ) {
2017-06-22 19:56:38 +02:00
// TODO : I suspect we shouldn ' t optimistically set hasSyncedTranscript .
// We could set this in a success handler for [ sendSyncTranscriptForMessage : ] .
[ message updateWithHasSyncedTranscript : YES ] ;
[ self sendSyncTranscriptForMessage : message ] ;
2016-10-14 23:00:29 +02:00
}
2017-05-09 20:38:49 +02:00
[ OWSDisappearingMessagesJob setExpirationForMessage : message ] ;
2016-10-14 23:00:29 +02:00
}
- ( void ) becomeConsistentWithDisappearingConfigurationForMessage : ( TSOutgoingMessage * ) outgoingMessage
{
2017-05-09 20:38:49 +02:00
[ OWSDisappearingMessagesJob becomeConsistentWithConfigurationForMessage : outgoingMessage
contactsManager : self . contactsManager ] ;
2016-10-14 23:00:29 +02:00
}
- ( void ) handleSendToMyself : ( TSOutgoingMessage * ) outgoingMessage
{
2016-10-28 19:18:46 +02:00
[ self handleMessageSentLocally : outgoingMessage ] ;
if ( ! ( outgoingMessage . body || outgoingMessage . hasAttachments ) ) {
DDLogDebug (
@ "%@ Refusing to make incoming copy of non-standard message sent to self:%@" , self . tag , outgoingMessage ) ;
return ;
}
2017-03-23 21:03:46 +01:00
// Getting the local number uses a transaction , so we need to do that before we
// create a new transaction to avoid deadlock .
NSString * contactId = [ TSAccountManager localNumber ] ;
2016-10-14 23:00:29 +02:00
[ self . dbConnection readWriteWithBlock : ^ ( YapDatabaseReadWriteTransaction * transaction ) {
TSContactThread * cThread =
2017-03-23 21:03:46 +01:00
[ TSContactThread getOrCreateThreadWithContactId : contactId transaction : transaction ] ;
2016-10-14 23:00:29 +02:00
[ cThread saveWithTransaction : transaction ] ;
2017-09-21 17:55:25 +02:00
// We want the incoming message to appear after the outgoing message .
2016-10-14 23:00:29 +02:00
TSIncomingMessage * incomingMessage =
[ [ TSIncomingMessage alloc ] initWithTimestamp : ( outgoingMessage . timestamp + 1 )
inThread : cThread
authorId : [ cThread contactIdentifier ]
2017-02-16 00:32:27 +01:00
sourceDeviceId : [ OWSDevice currentDeviceId ]
2016-10-14 23:00:29 +02:00
messageBody : outgoingMessage . body
attachmentIds : outgoingMessage . attachmentIds
expiresInSeconds : outgoingMessage . expiresInSeconds ] ;
[ incomingMessage saveWithTransaction : transaction ] ;
} ] ;
}
- ( void ) sendSyncTranscriptForMessage : ( TSOutgoingMessage * ) message
2016-10-08 01:17:38 +02:00
{
2016-10-14 23:00:29 +02:00
OWSOutgoingSentMessageTranscript * sentMessageTranscript =
[ [ OWSOutgoingSentMessageTranscript alloc ] initWithOutgoingMessage : message ] ;
[ self sendMessage : sentMessageTranscript
recipient : [ SignalRecipient selfRecipient ]
thread : message . thread
attempts : OWSMessageSenderRetryAttempts
2016-10-08 01:17:38 +02:00
success : ^ {
2016-10-14 23:00:29 +02:00
DDLogInfo ( @ "Succesfully sent sync transcript." ) ;
2016-10-08 01:17:38 +02:00
}
2017-04-14 16:25:52 +02:00
failure : ^ ( NSError * error ) {
2017-04-05 01:44:14 +02:00
// FIXME : We don ' t yet honor the isRetryable flag here , since sendSyncTranscriptForMessage
// isn ' t yet wrapped in our retryable SendMessageOperation . Addressing this would require
// a refactor to the MessageSender . Note that we * do * however continue to respect the
// OWSMessageSenderRetryAttempts , which is an "inner" retry loop , encompassing only the
// messaging API .
2017-04-14 16:25:52 +02:00
DDLogInfo ( @ "Failed to send sync transcript: %@ (isRetryable: %d)" , error , [ error isRetryable ] ) ;
2016-10-08 01:17:38 +02:00
} ] ;
}
2016-10-14 23:00:29 +02:00
- ( NSArray < NSDictionary * > * ) deviceMessages : ( TSOutgoingMessage * ) message
forRecipient : ( SignalRecipient * ) recipient
{
2017-09-27 23:13:29 +02:00
OWSAssert ( message ) ;
OWSAssert ( recipient ) ;
2016-10-14 23:00:29 +02:00
NSMutableArray * messagesArray = [ NSMutableArray arrayWithCapacity : recipient . devices . count ] ;
2017-08-01 23:01:07 +02:00
NSData * plainText = [ message buildPlainTextData : recipient ] ;
2017-07-28 23:58:40 +02:00
DDLogDebug ( @ "%@ built message: %@ plainTextData.length: %lu" , self . tag , [ message class ] , ( unsigned long ) plainText . length ) ;
2016-10-14 23:00:29 +02:00
for ( NSNumber * deviceNumber in recipient . devices ) {
@ try {
2017-01-23 00:09:38 +01:00
__block NSDictionary * messageDict ;
2017-01-31 15:46:25 +01:00
__block NSException * encryptionException ;
2017-01-23 00:09:38 +01:00
// Mutating session state is not thread safe , so we operate on a serial queue , shared with decryption
// operations .
2017-04-06 22:29:12 +02:00
dispatch_sync ( [ OWSDispatch sessionStoreQueue ] , ^ {
2017-01-31 15:46:25 +01:00
@ try {
messageDict = [ self encryptedMessageWithPlaintext : plainText
toRecipient : recipient . uniqueId
deviceId : deviceNumber
2017-09-27 23:13:29 +02:00
keyingStorage : self . storageManager
isSilent : message . isSilent ] ;
2017-01-31 15:46:25 +01:00
} @ catch ( NSException * exception ) {
encryptionException = exception ;
}
2017-01-23 00:09:38 +01:00
} ) ;
2017-09-13 22:48:38 +02:00
2017-01-31 15:46:25 +01:00
if ( encryptionException ) {
DDLogInfo ( @ "%@ Exception during encryption: %@" , self . tag , encryptionException ) ;
@ throw encryptionException ;
}
2017-01-23 00:09:38 +01:00
2016-10-14 23:00:29 +02:00
if ( messageDict ) {
[ messagesArray addObject : messageDict ] ;
} else {
@ throw [ NSException exceptionWithName : InvalidMessageException
reason : @ "Failed to encrypt message"
userInfo : nil ] ;
}
} @ catch ( NSException * exception ) {
if ( [ exception . name isEqualToString : OWSMessageSenderInvalidDeviceException ] ) {
[ recipient removeDevices : [ NSSet setWithObject : deviceNumber ] ] ;
} else {
@ throw exception ;
}
}
}
return [ messagesArray copy ] ;
}
- ( NSDictionary * ) encryptedMessageWithPlaintext : ( NSData * ) plainText
toRecipient : ( NSString * ) identifier
deviceId : ( NSNumber * ) deviceNumber
keyingStorage : ( TSStorageManager * ) storage
2017-09-27 23:13:29 +02:00
isSilent : ( BOOL ) isSilent
2016-10-14 23:00:29 +02:00
{
2017-09-27 23:13:29 +02:00
OWSAssert ( plainText ) ;
OWSAssert ( identifier . length > 0 ) ;
OWSAssert ( deviceNumber ) ;
OWSAssert ( storage ) ;
2016-10-14 23:00:29 +02:00
if ( ! [ storage containsSession : identifier deviceId : [ deviceNumber intValue ] ] ) {
__block dispatch_semaphore _t sema = dispatch_semaphore _create ( 0 ) ;
2017-07-24 16:35:18 +02:00
__block PreKeyBundle * _Nullable bundle ;
__block NSException * _Nullable exception ;
2016-10-14 23:00:29 +02:00
[ self . networkManager makeRequest : [ [ TSRecipientPrekeyRequest alloc ] initWithRecipient : identifier
deviceId : [ deviceNumber stringValue ] ]
success : ^ ( NSURLSessionDataTask * task , id responseObject ) {
bundle = [ PreKeyBundle preKeyBundleFromDictionary : responseObject forDeviceNumber : deviceNumber ] ;
dispatch_semaphore _signal ( sema ) ;
}
failure : ^ ( NSURLSessionDataTask * task , NSError * error ) {
2017-07-24 17:06:19 +02:00
if ( ! IsNSErrorNetworkFailure ( error ) ) {
2017-07-27 18:29:05 +02:00
OWSProdError ( [ OWSAnalyticsEvents messageSenderErrorRecipientPrekeyRequestFailed ] ) ;
2017-07-24 17:06:19 +02:00
}
2017-07-21 22:22:07 +02:00
DDLogError ( @ "Server replied to PreKeyBundle request with error: %@" , error ) ;
2016-10-14 23:00:29 +02:00
NSHTTPURLResponse * response = ( NSHTTPURLResponse * ) task . response ;
if ( response . statusCode = = 404 ) {
2016-11-03 21:10:06 +01:00
// Can ' t throw exception from within callback as it ' s probabably a different thread .
exception = [ NSException exceptionWithName : OWSMessageSenderInvalidDeviceException
reason : @ "Device not registered"
userInfo : nil ] ;
2016-11-10 15:59:07 +01:00
} else if ( response . statusCode = = 413 ) {
// Can ' t throw exception from within callback as it ' s probabably a different thread .
exception = [ NSException exceptionWithName : OWSMessageSenderRateLimitedException
reason : @ "Too many prekey requests"
userInfo : nil ] ;
2016-10-14 23:00:29 +02:00
}
dispatch_semaphore _signal ( sema ) ;
} ] ;
dispatch_semaphore _wait ( sema , DISPATCH_TIME _FOREVER ) ;
2016-11-03 21:10:06 +01:00
if ( exception ) {
@ throw exception ;
}
2016-10-14 23:00:29 +02:00
if ( ! bundle ) {
@ throw [ NSException exceptionWithName : InvalidVersionException
reason : @ "Can't get a prekey bundle from the server with required information"
userInfo : nil ] ;
} else {
SessionBuilder * builder = [ [ SessionBuilder alloc ] initWithSessionStore : storage
preKeyStore : storage
signedPreKeyStore : storage
2017-06-06 20:12:50 +02:00
identityKeyStore : [ OWSIdentityManager sharedManager ]
2016-10-14 23:00:29 +02:00
recipientId : identifier
deviceId : [ deviceNumber intValue ] ] ;
@ try {
2016-11-01 21:47:22 +01:00
// Mutating session state is not thread safe .
@ synchronized ( self ) {
[ builder processPrekeyBundle : bundle ] ;
}
2016-10-14 23:00:29 +02:00
} @ catch ( NSException * exception ) {
if ( [ exception . name isEqualToString : UntrustedIdentityKeyException ] ) {
@ throw [ NSException
exceptionWithName : UntrustedIdentityKeyException
reason : nil
userInfo : @ { TSInvalidPreKeyBundleKey : bundle , TSInvalidRecipientKey : identifier } ] ;
}
@ throw exception ;
}
}
}
SessionCipher * cipher = [ [ SessionCipher alloc ] initWithSessionStore : storage
preKeyStore : storage
signedPreKeyStore : storage
2017-06-06 20:12:50 +02:00
identityKeyStore : [ OWSIdentityManager sharedManager ]
2016-10-14 23:00:29 +02:00
recipientId : identifier
deviceId : [ deviceNumber intValue ] ] ;
2017-01-23 00:09:38 +01:00
id < CipherMessage > encryptedMessage = [ cipher encryptMessage : [ plainText paddedMessageBody ] ] ;
2016-10-14 23:00:29 +02:00
NSData * serializedMessage = encryptedMessage . serialized ;
TSWhisperMessageType messageType = [ self messageTypeForCipherMessage : encryptedMessage ] ;
2017-06-09 04:23:18 +02:00
OWSMessageServiceParams * messageParams = [ [ OWSMessageServiceParams alloc ] initWithType : messageType
recipientId : identifier
device : [ deviceNumber intValue ]
content : serializedMessage
2017-09-27 23:13:29 +02:00
isSilent : isSilent
2017-06-09 04:23:18 +02:00
registrationId : cipher . remoteRegistrationId ] ;
2016-10-14 23:00:29 +02:00
NSError * error ;
NSDictionary * jsonDict = [ MTLJSONAdapter JSONDictionaryFromModel : messageParams error : & error ] ;
if ( error ) {
2017-07-27 18:29:05 +02:00
OWSProdError ( [ OWSAnalyticsEvents messageSendErrorCouldNotSerializeMessageJson ] ) ;
2016-10-14 23:00:29 +02:00
return nil ;
}
return jsonDict ;
}
- ( TSWhisperMessageType ) messageTypeForCipherMessage : ( id < CipherMessage > ) cipherMessage
{
if ( [ cipherMessage isKindOfClass : [ PreKeyWhisperMessage class ] ] ) {
return TSPreKeyWhisperMessageType ;
} else if ( [ cipherMessage isKindOfClass : [ WhisperMessage class ] ] ) {
return TSEncryptedWhisperMessageType ;
}
return TSUnknownMessageType ;
}
- ( void ) saveGroupMessage : ( TSOutgoingMessage * ) message inThread : ( TSThread * ) thread
{
if ( message . groupMetaMessage = = TSGroupMessageDeliver ) {
2017-04-11 22:57:28 +02:00
// TODO : Why is this necessary ?
[ message save ] ;
2016-10-14 23:00:29 +02:00
} else if ( message . groupMetaMessage = = TSGroupMessageQuit ) {
2017-09-21 23:25:13 +02:00
[ [ [ TSInfoMessage alloc ] initWithTimestamp : message . timestamp
2016-10-14 23:00:29 +02:00
inThread : thread
messageType : TSInfoMessageTypeGroupQuit
customMessage : message . customMessage ] save ] ;
} else {
2017-09-21 23:25:13 +02:00
[ [ [ TSInfoMessage alloc ] initWithTimestamp : message . timestamp
2016-10-14 23:00:29 +02:00
inThread : thread
messageType : TSInfoMessageTypeGroupUpdate
customMessage : message . customMessage ] save ] ;
}
}
2017-06-08 05:21:25 +02:00
// Called when the server indicates that the devices no longer exist - e . g . when the remote recipient has reinstalled .
2017-04-07 01:11:04 +02:00
- ( void ) handleStaleDevicesWithResponse : ( NSData * ) responseData
recipientId : ( NSString * ) identifier
completion : ( void ( ^ ) ( ) ) completionHandler
2016-10-14 23:00:29 +02:00
{
dispatch_async ( [ OWSDispatch sendingQueue ] , ^ {
NSDictionary * serialization = [ NSJSONSerialization JSONObjectWithData : responseData options : 0 error : nil ] ;
NSArray * devices = serialization [ @ "staleDevices" ] ;
if ( ! ( [ devices count ] > 0 ) ) {
return ;
}
2017-04-07 01:11:04 +02:00
dispatch_async ( [ OWSDispatch sessionStoreQueue ] , ^ {
for ( NSUInteger i = 0 ; i < [ devices count ] ; i + + ) {
int deviceNumber = [ devices [ i ] intValue ] ;
[ [ TSStorageManager sharedManager ] deleteSessionForContact : identifier deviceId : deviceNumber ] ;
}
completionHandler ( ) ;
} ) ;
2016-10-14 23:00:29 +02:00
} ) ;
}
# pragma mark - Logging
+ ( NSString * ) tag
{
return [ NSString stringWithFormat : @ "[%@]" , self . class ] ;
}
- ( NSString * ) tag
{
return self . class . tag ;
}
2016-10-08 01:17:38 +02:00
@ end
NS_ASSUME _NONNULL _END