Explain send failures for text and media messages

Motivation
----------
We were often swallowing errors or yielding generic errors when it would
be better to provide specific errors.

We also didn't create an attachment when attachments failed to send,
making it impossible to show the user what was happening with an
in-progress or failed attachment.

Primary Changes
---------------
- Funnel all message sending through MessageSender, and remove message sending
  from MessagesManager.
  - Record most recent sending error so we can expose it in the UI
  - Can resend attachments.
  - Update message status for attachments, just like text messages
- Extracted UploadingService from MessagesManager
  - Saving attachment stream before uploading gives uniform API for send vs.
    resend
  - update status for downloading transcript attachments
- TSAttachments have a local id, separate from the server allocated id
  This allows us to save the attachment before the allocation request. Which is
  is good because:
  1. can show feedback to user faster.
  2. allows us to show an error when allocation fails.

Code Cleanup
------------
- Replaced a lot of global singleton access with injected dependencies to make
  for easier testing.
- Never save group meta messages. Rather than checking before (hopefully) every
  save, do it in the save method.
- Don't use callbacks for sync code.
- Handle errors on writing attachment data
- Fix old long broken tests that weren't even running. =(
- Removed dead code
- Use constants vs define
- Port flaky travis fixes from Signal-iOS

// FREEBIE
This commit is contained in:
Michael Kirk 2016-10-14 17:00:29 -04:00
parent d4c55d6940
commit 4ba1e86ec1
69 changed files with 2323 additions and 1818 deletions

View File

@ -2,6 +2,9 @@ language: objective-c
osx_image: xcode8
env:
-EARLY_START_SIMULATOR=1 # early starting simulator reduces false negatives due to test timeouts
before_install:
- brew update
- gem install cocoapods xcpretty --no-ri --no-rdoc

View File

@ -35,7 +35,7 @@ PODS:
- ProtocolBuffers (1.9.10)
- Reachability (3.2)
- SAMKeychain (1.5.0)
- SignalServiceKit (0.2.0):
- SignalServiceKit (0.3.0):
- '25519'
- AFNetworking
- AxolotlKit
@ -130,7 +130,7 @@ SPEC CHECKSUMS:
ProtocolBuffers: d088180c10072b3d24a9939a6314b7b9bcc2340b
Reachability: 33e18b67625424e47b6cde6d202dce689ad7af96
SAMKeychain: 1fc9ae02f576365395758b12888c84704eebc423
SignalServiceKit: 4e7a552635e10f4d94f0a047fc6554e932340b30
SignalServiceKit: 8b115cfd63f9b814fa03fe61fd5d38ef9a548460
SocketRocket: 3f77ec2104cc113add553f817ad90a77114f5d43
SQLCipher: 4c768761421736a247ed6cf412d9045615d53dff
TwistedOakCollapsingFutures: f359b90f203e9ab13dfb92c9ff41842a7fe1cd0c

View File

@ -16,6 +16,7 @@
453E1FD81DA83E1000DDD7B7 /* OWSFakeContactsManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 453E1FD71DA83E1000DDD7B7 /* OWSFakeContactsManager.m */; };
453E1FDB1DA83EFB00DDD7B7 /* OWSFakeContactsUpdater.m in Sources */ = {isa = PBXBuildFile; fileRef = 453E1FDA1DA83EFB00DDD7B7 /* OWSFakeContactsUpdater.m */; };
454021ED1D960ABF00F2126D /* OWSDisappearingMessageFinderTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 454021EC1D960ABF00F2126D /* OWSDisappearingMessageFinderTest.m */; };
454092FA1DB7AFDE00579DE1 /* OWSFakeNetworkManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 454092F91DB7AFDE00579DE1 /* OWSFakeNetworkManager.m */; };
45458B751CC342B600A02153 /* SignedPreKeyDeletionTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 45458B6A1CC342B600A02153 /* SignedPreKeyDeletionTests.m */; };
45458B761CC342B600A02153 /* TSAttachmentsTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 45458B6C1CC342B600A02153 /* TSAttachmentsTest.m */; };
45458B771CC342B600A02153 /* TSMessageStorageTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 45458B6E1CC342B600A02153 /* TSMessageStorageTests.m */; };
@ -66,6 +67,8 @@
453E1FD91DA83EFB00DDD7B7 /* OWSFakeContactsUpdater.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = OWSFakeContactsUpdater.h; path = ../../../tests/TestSupport/Fakes/OWSFakeContactsUpdater.h; sourceTree = "<group>"; };
453E1FDA1DA83EFB00DDD7B7 /* OWSFakeContactsUpdater.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = OWSFakeContactsUpdater.m; path = ../../../tests/TestSupport/Fakes/OWSFakeContactsUpdater.m; sourceTree = "<group>"; };
454021EC1D960ABF00F2126D /* OWSDisappearingMessageFinderTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = OWSDisappearingMessageFinderTest.m; path = ../../../tests/Messages/OWSDisappearingMessageFinderTest.m; sourceTree = "<group>"; };
454092F81DB7AFDE00579DE1 /* OWSFakeNetworkManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = OWSFakeNetworkManager.h; path = ../../../tests/TestSupport/Fakes/OWSFakeNetworkManager.h; sourceTree = "<group>"; };
454092F91DB7AFDE00579DE1 /* OWSFakeNetworkManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = OWSFakeNetworkManager.m; path = ../../../tests/TestSupport/Fakes/OWSFakeNetworkManager.m; sourceTree = "<group>"; };
45458B6A1CC342B600A02153 /* SignedPreKeyDeletionTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SignedPreKeyDeletionTests.m; sourceTree = "<group>"; };
45458B6C1CC342B600A02153 /* TSAttachmentsTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TSAttachmentsTest.m; sourceTree = "<group>"; };
45458B6E1CC342B600A02153 /* TSMessageStorageTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TSMessageStorageTests.m; sourceTree = "<group>"; };
@ -136,6 +139,8 @@
453E1FD71DA83E1000DDD7B7 /* OWSFakeContactsManager.m */,
453E1FD91DA83EFB00DDD7B7 /* OWSFakeContactsUpdater.h */,
453E1FDA1DA83EFB00DDD7B7 /* OWSFakeContactsUpdater.m */,
454092F81DB7AFDE00579DE1 /* OWSFakeNetworkManager.h */,
454092F91DB7AFDE00579DE1 /* OWSFakeNetworkManager.m */,
);
name = Fakes;
sourceTree = "<group>";
@ -549,6 +554,7 @@
45731A6C1DA87AA1007E22AA /* TSOutgoingMessageTest.m in Sources */,
6323E339D5B8F4CB77EB3ED4 /* SignalRecipientTest.m in Sources */,
6323E1F7730289398452E5C5 /* OWSFingerprintTest.m in Sources */,
454092FA1DB7AFDE00579DE1 /* OWSFakeNetworkManager.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};

View File

@ -1,9 +1,11 @@
# Make sure we're failing even though we pipe to xcpretty
SHELL=/bin/bash -o pipefail -o errexit
BUILD_DESTINATION = platform=iOS Simulator,name=iPhone 6,OS=9.3
WORKING_DIR = Example/TSKitiOSTestApp
SCHEME = TSKitiOSTestApp
DEVICE_UUID:=$(shell xcrun instruments -s | grep -o "iPhone 6 (9.3) \[.*\]" | grep -o "\[.*\]" | sed "s/^\[\(.*\)\]$$/\1/")
BUILD_DESTINATION = platform=iOS Simulator,id=${DEVICE_UUID}
XCODE_BUILD = xcrun xcodebuild -workspace $(SCHEME).xcworkspace -scheme $(SCHEME) -sdk iphonesimulator
.PHONY: build test retest clean
@ -20,14 +22,22 @@ build: pod_install
cd $(WORKING_DIR) && \
$(XCODE_BUILD) build | xcpretty
retest:
retest: optional_early_start_simulator
cd $(WORKING_DIR) && \
$(XCODE_BUILD) \
-destination '${BUILD_DESTINATION}' \
build test
test | xcpretty
clean:
cd $(WORKING_DIR) && \
$(XCODE_BUILD) \
clean | xcpretty
optional_early_start_simulator:
ifdef EARLY_START_SIMULATOR
echo "Waiting for simulator to start to help with testing timeouts" &&\
xcrun instruments -w '${DEVICE_UUID}' || true # xcrun can return irrelevant non-zeroes.
else
echo "Not waiting for simulator."
endif

View File

@ -8,7 +8,7 @@
Pod::Spec.new do |s|
s.name = "SignalServiceKit"
s.version = "0.2.0"
s.version = "0.3.0"
s.summary = "An Objective-C library for communicating with the Signal messaging service."
s.description = <<-DESC

View File

@ -11,9 +11,7 @@
+ (instancetype)sharedUpdater;
- (void)synchronousLookup:(NSString *)identifier
success:(void (^)(SignalRecipient *))success
failure:(void (^)(NSError *error))failure;
- (SignalRecipient *)synchronousLookup:(NSString *)identifier error:(NSError **)error;
- (void)lookupIdentifier:(NSString *)identifier
success:(void (^)(NSSet<NSString *> *matchedIds))success

View File

@ -22,38 +22,27 @@
return sharedInstance;
}
- (void)synchronousLookup:(NSString *)identifier
success:(void (^)(SignalRecipient *))success
failure:(void (^)(NSError *error))failure {
__block dispatch_semaphore_t sema = dispatch_semaphore_create(0);
__block SignalRecipient *recipient = nil;
__block NSError *error = nil;
- (SignalRecipient *)synchronousLookup:(NSString *)identifier error:(NSError **)error
{
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
__block SignalRecipient *recipient;
[self lookupIdentifier:identifier
success:^(NSSet<NSString *> *matchedIds) {
if ([matchedIds count] == 1) {
[[TSStorageManager sharedManager]
.dbConnection readWithBlock:^(YapDatabaseReadTransaction *_Nonnull transaction) {
recipient = [SignalRecipient recipientWithTextSecureIdentifier:identifier withTransaction:transaction];
}];
} else {
error = [NSError errorWithDomain:@"contactsmanager.notfound" code:NOTFOUND_ERROR userInfo:nil];
}
dispatch_semaphore_signal(sema);
if (matchedIds.count == 1) {
recipient = [SignalRecipient recipientWithTextSecureIdentifier:identifier];
} else {
*error = [NSError errorWithDomain:@"contactsmanager.notfound" code:NOTFOUND_ERROR userInfo:nil];
}
dispatch_semaphore_signal(sema);
}
failure:^(NSError *blockerror) {
error = blockerror;
dispatch_semaphore_signal(sema);
*error = blockerror;
dispatch_semaphore_signal(sema);
}];
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
if (error) {
SYNC_BLOCK_SAFE_RUN(failure, error);
} else {
SYNC_BLOCK_SAFE_RUN(success, recipient);
}
return;
return recipient;
}

View File

@ -1,14 +1,13 @@
//
// TSGroupThread.h
// TextSecureKit
//
// Created by Frederic Jacobs on 16/11/14.
// Copyright (c) 2014 Open Whisper Systems. All rights reserved.
//
#import "TSGroupModel.h"
#import "TSThread.h"
NS_ASSUME_NONNULL_BEGIN
@class TSAttachmentStream;
@interface TSGroupThread : TSThread
@property (nonatomic, strong) TSGroupModel *groupModel;
@ -18,7 +17,12 @@
+ (instancetype)getOrCreateThreadWithGroupIdData:(NSData *)groupId;
+ (instancetype)fetchWithGroupIdData:(NSData *)groupId;
+ (instancetype)threadWithGroupModel:(TSGroupModel *)groupModel transaction:(YapDatabaseReadTransaction *)transaction;
+ (NSString *)threadIdFromGroupId:(NSData *)groupId;
- (void)updateAvatarWithAttachmentStream:(TSAttachmentStream *)attachmentStream;
@end
NS_ASSUME_NONNULL_END

View File

@ -4,8 +4,11 @@
#import "TSGroupThread.h"
#import "NSData+Base64.h"
#import "SignalRecipient.h"
#import "TSAttachmentStream.h"
#import <YapDatabase/YapDatabaseTransaction.h>
NS_ASSUME_NONNULL_BEGIN
@implementation TSGroupThread
#define TSGroupThreadPrefix @"g"
@ -40,11 +43,6 @@
return [self fetchObjectWithUniqueID:[self threadIdFromGroupId:groupModel.groupId] transaction:transaction];
}
+ (instancetype)fetchWithGroupIdData:(NSData *)groupId
{
return [self fetchObjectWithUniqueID:[self threadIdFromGroupId:groupId]];
}
+ (instancetype)getOrCreateThreadWithGroupIdData:(NSData *)groupId
{
TSGroupThread *thread = [self fetchObjectWithUniqueID:[self threadIdFromGroupId:groupId]];
@ -87,4 +85,16 @@
return self.groupModel.groupName ? self.groupModel.groupName : NSLocalizedString(@"NEW_GROUP_DEFAULT_TITLE", @"");
}
- (void)updateAvatarWithAttachmentStream:(TSAttachmentStream *)attachmentStream
{
self.groupModel.groupImage = [attachmentStream image];
[self save];
// Avatars are stored directly in the database, so there's no need
// to keep the attachment around after assigning the image.
[attachmentStream remove];
}
@end
NS_ASSUME_NONNULL_END

View File

@ -4,15 +4,18 @@
NS_ASSUME_NONNULL_BEGIN
@class OWSIncomingSentMessageTranscript;
@class TSMessagesManager;
@class OWSMessageSender;
@class TSNetworkManager;
@class TSAttachmentStream;
@interface OWSRecordTranscriptJob : NSObject
- (instancetype)init NS_UNAVAILABLE;
- (instancetype)initWithMessagesManager:(TSMessagesManager *)messagesManager
incomingSentMessageTranscript:(OWSIncomingSentMessageTranscript *)incomingSendtMessageTranscript NS_DESIGNATED_INITIALIZER;
- (instancetype)initWithIncomingSentMessageTranscript:(OWSIncomingSentMessageTranscript *)incomingSentMessageTranscript
messageSender:(OWSMessageSender *)messageSender
networkManager:(TSNetworkManager *)networkManager NS_DESIGNATED_INITIALIZER;
- (void)run;
- (void)runWithAttachmentHandler:(void (^)(TSAttachmentStream *attachmentStream))attachmentHandler;
@end

View File

@ -5,7 +5,7 @@
#import "OWSAttachmentsProcessor.h"
#import "OWSDisappearingMessagesJob.h"
#import "OWSIncomingSentMessageTranscript.h"
#import "TSMessagesManager+sendMessages.h"
#import "OWSMessageSender.h"
#import "TSOutgoingMessage.h"
#import "TSStorageManager.h"
@ -13,39 +13,41 @@ NS_ASSUME_NONNULL_BEGIN
@interface OWSRecordTranscriptJob ()
@property (nonatomic, readonly) TSMessagesManager *messagesManager;
@property (nonatomic, readonly) OWSIncomingSentMessageTranscript *incomingSendtMessageTranscript;
@property (nonatomic, readonly) OWSIncomingSentMessageTranscript *incomingSentMessageTranscript;
@property (nonatomic, readonly) OWSMessageSender *messageSender;
@property (nonatomic, readonly) TSNetworkManager *networkManager;
@end
@implementation OWSRecordTranscriptJob
- (instancetype)initWithMessagesManager:(TSMessagesManager *)messagesManager
incomingSentMessageTranscript:(OWSIncomingSentMessageTranscript *)incomingSendtMessageTranscript
- (instancetype)initWithIncomingSentMessageTranscript:(OWSIncomingSentMessageTranscript *)incomingSentMessageTranscript
messageSender:(OWSMessageSender *)messageSender
networkManager:(TSNetworkManager *)networkManager
{
self = [super init];
if (!self) {
return self;
}
_messagesManager = messagesManager;
_incomingSendtMessageTranscript = incomingSendtMessageTranscript;
_incomingSentMessageTranscript = incomingSentMessageTranscript;
_messageSender = messageSender;
_networkManager = networkManager;
return self;
}
- (void)run
- (void)runWithAttachmentHandler:(void (^)(TSAttachmentStream *attachmentStream))attachmentHandler
{
OWSIncomingSentMessageTranscript *transcript = self.incomingSendtMessageTranscript;
OWSIncomingSentMessageTranscript *transcript = self.incomingSentMessageTranscript;
DDLogDebug(@"%@ Recording transcript: %@", self.tag, transcript);
TSThread *thread = transcript.thread;
OWSAttachmentsProcessor *attachmentsProcessor =
[[OWSAttachmentsProcessor alloc] initWithAttachmentPointersProtos:transcript.attachmentPointerProtos
timestamp:transcript.timestamp
relay:transcript.relay
avatarGroupId:transcript.groupId
inThread:thread
messagesManager:self.messagesManager];
[[OWSAttachmentsProcessor alloc] initWithAttachmentProtos:transcript.attachmentPointerProtos
timestamp:transcript.timestamp
relay:transcript.relay
thread:thread
networkManager:self.networkManager];
// TODO group updates. Currently desktop doesn't support group updates, so not a problem yet.
TSOutgoingMessage *outgoingMessage =
@ -57,14 +59,21 @@ NS_ASSUME_NONNULL_BEGIN
expireStartedAt:transcript.expirationStartedAt];
if (transcript.isExpirationTimerUpdate) {
[self.messagesManager becomeConsistentWithDisappearingConfigurationForMessage:outgoingMessage];
[self.messageSender becomeConsistentWithDisappearingConfigurationForMessage:outgoingMessage];
// early return to avoid saving an empty incoming message.
return;
}
[self.messagesManager handleMessageSentRemotely:outgoingMessage sentAt:transcript.expirationStartedAt];
[self.messageSender handleMessageSentRemotely:outgoingMessage sentAt:transcript.expirationStartedAt];
[attachmentsProcessor fetchAttachmentsForMessageId:outgoingMessage.uniqueId];
[attachmentsProcessor
fetchAttachmentsForMessage:outgoingMessage
success:attachmentHandler
failure:^(NSError *_Nonnull error) {
DDLogError(@"%@ failed to fetch transcripts attachments for message: %@",
self.tag,
outgoingMessage);
}];
// If there is an attachment + text, render the text here, as Signal-iOS renders two messages.
if (attachmentsProcessor.hasSupportedAttachments && transcript.body && ![transcript.body isEqualToString:@""]) {

View File

@ -3,13 +3,13 @@
NS_ASSUME_NONNULL_BEGIN
@class TSMessagesManager;
@class OWSMessageSender;
@class TSIncomingMessage;
@interface OWSSendReadReceiptsJob : NSObject
- (instancetype)init NS_UNAVAILABLE;
- (instancetype)initWithMessagesManager:(TSMessagesManager *)messagesManager NS_DESIGNATED_INITIALIZER;
- (instancetype)initWithMessageSender:(OWSMessageSender *)messageSender NS_DESIGNATED_INITIALIZER;
- (void)runWith:(TSIncomingMessage *)message;

View File

@ -2,18 +2,18 @@
// Copyright © 2016 Open Whisper Systems. All rights reserved.
#import "OWSSendReadReceiptsJob.h"
#import "OWSMessageSender.h"
#import "OWSReadReceipt.h"
#import "OWSReadReceiptsMessage.h"
#import "TSContactThread.h"
#import "TSIncomingMessage.h"
#import "TSMessagesManager+sendMessages.h"
NS_ASSUME_NONNULL_BEGIN
@interface OWSSendReadReceiptsJob ()
@property (atomic) NSMutableArray<OWSReadReceipt *> *readReceiptsQueue;
@property (nonatomic, readonly) TSMessagesManager *messagesManager;
@property (nonatomic, readonly) OWSMessageSender *messageSender;
@property BOOL isObserving;
@end
@ -25,7 +25,7 @@ NS_ASSUME_NONNULL_BEGIN
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
- (instancetype)initWithMessagesManager:(TSMessagesManager *)messagesManager
- (instancetype)initWithMessageSender:(OWSMessageSender *)messageSender
{
self = [super init];
if (!self) {
@ -33,7 +33,7 @@ NS_ASSUME_NONNULL_BEGIN
}
_readReceiptsQueue = [NSMutableArray new];
_messagesManager = messagesManager;
_messageSender = messageSender;
_isObserving = NO;
return self;
@ -81,16 +81,27 @@ NS_ASSUME_NONNULL_BEGIN
{
OWSReadReceiptsMessage *message = [[OWSReadReceiptsMessage alloc] initWithReadReceipts:readReceipts];
[self.messagesManager sendMessage:message
inThread:nil
[self.messageSender sendMessage:message
success:^{
DDLogInfo(@"Successfully sent %ld read receipt", (unsigned long)readReceipts.count);
DDLogInfo(@"%@ Successfully sent %ld read receipt", self.tag, (unsigned long)readReceipts.count);
}
failure:^{
DDLogError(@"Failed to send read receipt");
failure:^(NSError *error) {
DDLogError(@"%@ Failed to send read receipt with error: %@", self.tag, error);
}];
}
#pragma mark - Logging
+ (NSString *)tag
{
return [NSString stringWithFormat:@"[%@]", self.class];
}
- (NSString *)tag
{
return self.class.tag;
}
@end
NS_ASSUME_NONNULL_END

View File

@ -2,9 +2,12 @@
NS_ASSUME_NONNULL_BEGIN
@class TSMessage;
@class TSThread;
@class TSMessagesManager;
@class TSNetworkManager;
@class OWSSignalServiceProtosAttachmentPointer;
@class TSAttachmentStream;
@class TSAttachmentPointer;
/**
* Given incoming attachment protos, determines which we support.
@ -12,19 +15,27 @@ NS_ASSUME_NONNULL_BEGIN
*/
@interface OWSAttachmentsProcessor : NSObject
@property (nonatomic, readonly) NSArray<NSString *> *attachmentIds;
@property (nullable, nonatomic, readonly) NSArray<NSString *> *attachmentIds;
@property (nonatomic, readonly) NSArray<NSString *> *supportedAttachmentIds;
@property (nonatomic, readonly) BOOL hasSupportedAttachments;
- (instancetype)initWithAttachmentPointersProtos:(NSArray<OWSSignalServiceProtosAttachmentPointer *> *)attachmentProtos
timestamp:(uint64_t)timestamp
relay:(nullable NSString *)relay
avatarGroupId:(nullable NSData *)avatarGroupId
inThread:(TSThread *)thread
messagesManager:(TSMessagesManager *)messagesManager;
- (instancetype)init NS_UNAVAILABLE;
- (void)fetchAttachmentsForMessageId:(nullable NSString *)messageId;
- (instancetype)initWithAttachmentProtos:(NSArray<OWSSignalServiceProtosAttachmentPointer *> *)attachmentProtos
timestamp:(uint64_t)timestamp
relay:(nullable NSString *)relay
thread:(TSThread *)thread
networkManager:(TSNetworkManager *)networkManager NS_DESIGNATED_INITIALIZER;
/*
* Retry fetching failed attachment download
*/
- (instancetype)initWithAttachmentPointer:(TSAttachmentPointer *)attachmentPointer
networkManager:(TSNetworkManager *)networkManager NS_DESIGNATED_INITIALIZER;
- (void)fetchAttachmentsForMessage:(nullable TSMessage *)message
success:(void (^)(TSAttachmentStream *attachmentStream))successHandler
failure:(void (^)(NSError *error))failureHandler;
@end
NS_ASSUME_NONNULL_END

View File

@ -1,58 +1,71 @@
// Copyright © 2016 Open Whisper Systems. All rights reserved.
#import "OWSAttachmentsProcessor.h"
#import "Cryptography.h"
#import "MIMETypeUtil.h"
#import "OWSDispatch.h"
#import "OWSError.h"
#import "OWSSignalServiceProtos.pb.h"
#import "TSAttachmentPointer.h"
#import "TSAttachmentRequest.h"
#import "TSAttachmentStream.h"
#import "TSGroupModel.h"
#import "TSGroupThread.h"
#import "TSInfoMessage.h"
#import "TSMessage.h"
#import "TSMessagesManager+attachments.h"
#import "TSMessagesManager.h"
#import "TSNetworkManager.h"
#import "TSThread.h"
#import <YapDatabase/YapDatabaseConnection.h>
NS_ASSUME_NONNULL_BEGIN
@interface OWSAttachmentsProcessor ()
@property (nonatomic, readonly) TSMessagesManager *messagesManager;
@property (nonatomic, readonly) TSNetworkManager *networkManager;
@property (nonatomic, readonly) NSArray<TSAttachmentPointer *> *supportedAttachmentPointers;
@end
@implementation OWSAttachmentsProcessor
- (instancetype)initWithAttachmentPointersProtos:(NSArray<OWSSignalServiceProtosAttachmentPointer *> *)attachmentProtos
timestamp:(uint64_t)timestamp
relay:(nullable NSString *)relay
avatarGroupId:(nullable NSData *)avatarGroupId
inThread:(TSThread *)thread
messagesManager:(TSMessagesManager *)messagesManager;
- (instancetype)initWithAttachmentPointer:(TSAttachmentPointer *)attachmentPointer
networkManager:(TSNetworkManager *)networkManager
{
self = [super init];
if (!self) {
return self;
}
_messagesManager = messagesManager;
_networkManager = networkManager;
_supportedAttachmentPointers = @[ attachmentPointer ];
_supportedAttachmentIds = @[ attachmentPointer.uniqueId ];
return self;
}
- (instancetype)initWithAttachmentProtos:(NSArray<OWSSignalServiceProtosAttachmentPointer *> *)attachmentProtos
timestamp:(uint64_t)timestamp
relay:(nullable NSString *)relay
thread:(TSThread *)thread
networkManager:(TSNetworkManager *)networkManager
{
self = [super init];
if (!self) {
return self;
}
_networkManager = networkManager;
NSMutableArray<NSString *> *attachmentIds = [NSMutableArray new];
NSMutableArray<TSAttachmentPointer *> *supportedAttachmentPointers = [NSMutableArray new];
NSMutableArray<NSString *> *supportedAttachmentIds = [NSMutableArray new];
for (OWSSignalServiceProtosAttachmentPointer *attachmentProto in attachmentProtos) {
TSAttachmentPointer *pointer;
if (avatarGroupId) {
pointer = [[TSAttachmentPointer alloc] initWithIdentifier:attachmentProto.id
key:attachmentProto.key
contentType:attachmentProto.contentType
relay:relay
avatarOfGroupId:avatarGroupId];
} else {
pointer = [[TSAttachmentPointer alloc] initWithIdentifier:attachmentProto.id
key:attachmentProto.key
contentType:attachmentProto.contentType
relay:relay];
}
TSAttachmentPointer *pointer = [[TSAttachmentPointer alloc] initWithServerId:attachmentProto.id
key:attachmentProto.key
contentType:attachmentProto.contentType
relay:relay];
[attachmentIds addObject:pointer.uniqueId];
@ -76,10 +89,140 @@ NS_ASSUME_NONNULL_BEGIN
return self;
}
- (void)fetchAttachmentsForMessageId:(nullable NSString *)messageId
- (void)fetchAttachmentsForMessage:(nullable TSMessage *)message
success:(void (^)(TSAttachmentStream *attachmentStream))successHandler
failure:(void (^)(NSError *error))failureHandler
{
for (TSAttachmentPointer *attachmentPointer in self.supportedAttachmentPointers) {
[self.messagesManager retrieveAttachment:attachmentPointer messageId:messageId];
[self retrieveAttachment:attachmentPointer message:message success:successHandler failure:failureHandler];
}
}
- (void)retrieveAttachment:(TSAttachmentPointer *)attachment
message:(nullable TSMessage *)message
success:(void (^)(TSAttachmentStream *attachmentStream))successHandler
failure:(void (^)(NSError *error))failureHandler
{
[self setAttachment:attachment isDownloadingInMessage:message];
void (^markAndHandleFailure)(NSError *) = ^(NSError *error) {
[self setAttachment:attachment didFailInMessage:message];
return failureHandler(error);
};
void (^markAndHandleSuccess)(TSAttachmentStream *attachmentStream) = ^(TSAttachmentStream *attachmentStream) {
successHandler(attachmentStream);
if (message) {
[message touch];
}
};
TSAttachmentRequest *attachmentRequest = [[TSAttachmentRequest alloc] initWithId:attachment.serverId relay:attachment.relay];
[self.networkManager makeRequest:attachmentRequest
success:^(NSURLSessionDataTask *task, id responseObject) {
if (![responseObject isKindOfClass:[NSDictionary class]]) {
DDLogError(@"%@ Failed retrieval of attachment. Response had unexpected format.",
self.tag);
NSError *error = OWSErrorMakeUnableToProcessServerResponseError();
return markAndHandleFailure(error);
}
NSString *location = [(NSDictionary *)responseObject objectForKey:@"location"];
if (!location) {
DDLogError(
@"%@ Failed retrieval of attachment. Response had no location.", self.tag);
NSError *error = OWSErrorMakeUnableToProcessServerResponseError();
return markAndHandleFailure(error);
}
dispatch_async([OWSDispatch attachmentsQueue], ^{
[self downloadFromLocation:location
pointer:attachment
success:^(NSData *_Nonnull encryptedData) {
[self decryptAttachmentData:encryptedData
pointer:attachment
success:markAndHandleSuccess
failure:markAndHandleFailure];
}
failure:markAndHandleFailure];
});
}
failure:^(NSURLSessionDataTask *task, NSError *error) {
DDLogError(@"Failed retrieval of attachment with error: %@", error);
return markAndHandleFailure(error);
}];
}
- (void)decryptAttachmentData:(NSData *)cipherText
pointer:(TSAttachmentPointer *)attachment
success:(void (^)(TSAttachmentStream *attachmentStream))successHandler
failure:(void (^)(NSError *error))failureHandler
{
NSData *plaintext = [Cryptography decryptAttachment:cipherText withKey:attachment.encryptionKey];
if (!plaintext) {
NSError *error = OWSErrorWithCodeDescription(OWSErrorCodeFailedToDecryptMessage, NSLocalizedString(@"ERROR_MESSAGE_INVALID_MESSAGE", @""));
return failureHandler(error);
}
TSAttachmentStream *stream = [[TSAttachmentStream alloc] initWithPointer:attachment];
NSError *error;
[stream writeData:plaintext error:&error];
if (error) {
DDLogError(@"%@ Failed writing attachment stream with error: %@", self.tag, error);
return failureHandler(error);
}
[stream save];
successHandler(stream);
}
- (void)downloadFromLocation:(NSString *)location
pointer:(TSAttachmentPointer *)pointer
success:(void (^)(NSData *encryptedData))successHandler
failure:(void (^)(NSError *error))failureHandler
{
AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
manager.requestSerializer = [AFHTTPRequestSerializer serializer];
[manager.requestSerializer setValue:OWSMimeTypeApplicationOctetStream forHTTPHeaderField:@"Content-Type"];
manager.responseSerializer = [AFHTTPResponseSerializer serializer];
manager.completionQueue = dispatch_get_main_queue();
// TODO stream this download rather than storing the entire blob.
[manager GET:location
parameters:nil
progress:nil // TODO show some progress!
success:^(NSURLSessionDataTask *_Nonnull task, id _Nullable responseObject) {
if (![responseObject isKindOfClass:[NSData class]]) {
DDLogError(@"%@ Failed retrieval of attachment. Response had unexpected format.", self.tag);
NSError *error = OWSErrorMakeUnableToProcessServerResponseError();
return failureHandler(error);
}
successHandler((NSData *)responseObject);
}
failure:^(NSURLSessionDataTask *_Nullable task, NSError *_Nonnull error) {
DDLogError(@"Failed to retrieve attachment with error: %@", error.description);
return failureHandler(error);
}];
}
- (void)setAttachment:(TSAttachmentPointer *)pointer isDownloadingInMessage:(nullable TSMessage *)message
{
pointer.downloading = YES;
[pointer save];
if (message) {
[message touch];
}
}
- (void)setAttachment:(TSAttachmentPointer *)pointer didFailInMessage:(nullable TSMessage *)message
{
pointer.downloading = NO;
pointer.failed = YES;
[pointer save];
if (message) {
[message touch];
}
}
@ -88,6 +231,8 @@ NS_ASSUME_NONNULL_BEGIN
return self.supportedAttachmentPointers.count > 0;
}
#pragma mark - Logging
+ (NSString *)tag
{
return [NSString stringWithFormat:@"[%@]", self.class];

View File

@ -1,24 +1,24 @@
//
// TSAttachment.h
// TextSecureKit
//
// Created by Frederic Jacobs on 12/11/14.
// Copyright (c) 2014 Open Whisper Systems. All rights reserved.
//
#import <Foundation/Foundation.h>
#import "TSYapDatabaseObject.h"
@interface TSAttachment : TSYapDatabaseObject
NS_ASSUME_NONNULL_BEGIN
- (NSNumber *)identifier;
@interface TSAttachment : TSYapDatabaseObject {
@property (nonatomic, readonly) NSData *encryptionKey;
@protected
NSString *_contentType;
}
@property (atomic, readwrite) UInt64 serverId;
@property (atomic, readwrite) NSData *encryptionKey;
@property (nonatomic, readonly) NSString *contentType;
- (instancetype)initWithIdentifier:(NSString *)identifier
encryptionKey:(NSData *)encryptionKey
contentType:(NSString *)contentType;
- (instancetype)initWithServerId:(UInt64)serverId
encryptionKey:(NSData *)encryptionKey
contentType:(NSString *)contentType;
@end
NS_ASSUME_NONNULL_END

View File

@ -4,18 +4,53 @@
#import "TSAttachment.h"
#import "MIMETypeUtil.h"
NS_ASSUME_NONNULL_BEGIN
NSUInteger const TSAttachmentSchemaVersion = 2;
@interface TSAttachment ()
@property (nonatomic, readonly) NSUInteger attachmentSchemaVersion;
@end
@implementation TSAttachment
- (instancetype)initWithIdentifier:(NSString *)identifier
encryptionKey:(NSData *)encryptionKey
contentType:(NSString *)contentType {
self = [super initWithUniqueId:identifier];
if (self) {
_encryptionKey = encryptionKey;
_contentType = contentType;
- (instancetype)initWithServerId:(UInt64)serverId
encryptionKey:(NSData *)encryptionKey
contentType:(NSString *)contentType
{
self = [super init];
if (!self) {
return self;
}
_serverId = serverId;
_encryptionKey = encryptionKey;
_contentType = contentType;
_attachmentSchemaVersion = TSAttachmentSchemaVersion;
return self;
}
- (nullable instancetype)initWithCoder:(NSCoder *)coder
{
self = [super initWithCoder:coder];
if (!self) {
return self;
}
if (_attachmentSchemaVersion < 2) {
if (!_serverId) {
_serverId = [self.uniqueId integerValue];
if (!_serverId) {
DDLogError(@"%@ failed to parse legacy uniqueId:%@ as integer.", self.tag, self.uniqueId);
}
}
}
_attachmentSchemaVersion = TSAttachmentSchemaVersion;
return self;
}
@ -23,12 +58,6 @@
return @"TSAttachements";
}
- (NSNumber *)identifier {
NSNumberFormatter *f = [[NSNumberFormatter alloc] init];
[f setNumberStyle:NSNumberFormatterDecimalStyle];
return [f numberFromString:self.uniqueId];
}
- (NSString *)description {
NSString *attachmentString = NSLocalizedString(@"ATTACHMENT", nil);
@ -45,4 +74,18 @@
return attachmentString;
}
#pragma mark - Logging
+ (NSString *)tag
{
return [NSString stringWithFormat:@"[%@]", self.class];
}
- (NSString *)tag
{
return self.class.tag;
}
@end
NS_ASSUME_NONNULL_END

View File

@ -3,26 +3,22 @@
#import "TSAttachment.h"
NS_ASSUME_NONNULL_BEGIN
/**
* A TSAttachmentPointer is a yet-to-be-downloaded attachment.
*/
@interface TSAttachmentPointer : TSAttachment
- (instancetype)initWithIdentifier:(uint64_t)identifier
key:(NSData *)key
contentType:(NSString *)contentType
relay:(NSString *)relay NS_DESIGNATED_INITIALIZER;
- (instancetype)initWithServerId:(UInt64)serverId
key:(NSData *)key
contentType:(NSString *)contentType
relay:(NSString *)relay NS_DESIGNATED_INITIALIZER;
- (instancetype)initWithIdentifier:(uint64_t)identifier
key:(NSData *)key
contentType:(NSString *)contentType
relay:(NSString *)relay
avatarOfGroupId:(NSData *)avatarOfGroupId;
@property NSString *relay;
@property NSData *avatarOfGroupId;
@property (getter=isDownloading) BOOL downloading;
@property (getter=hasFailed) BOOL failed;
@property (nonatomic, readonly) NSString *relay;
@property (atomic, readwrite, getter=isDownloading) BOOL downloading;
@property (atomic, readwrite, getter=hasFailed) BOOL failed;
@end
NS_ASSUME_NONNULL_END

View File

@ -3,43 +3,31 @@
#import "TSAttachmentPointer.h"
NS_ASSUME_NONNULL_BEGIN
@implementation TSAttachmentPointer
- (instancetype)initWithIdentifier:(uint64_t)identifier
key:(NSData *)key
contentType:(NSString *)contentType
relay:(NSString *)relay {
self = [super initWithIdentifier:[[NSNumber numberWithUnsignedLongLong:identifier] stringValue]
encryptionKey:key
contentType:contentType];
if (self) {
_failed = FALSE;
_downloading = FALSE;
_relay = relay;
- (instancetype)initWithServerId:(UInt64)serverId
key:(NSData *)key
contentType:(NSString *)contentType
relay:(NSString *)relay
{
self = [super initWithServerId:serverId encryptionKey:key contentType:contentType];
if (!self) {
return self;
}
_failed = NO;
_downloading = NO;
_relay = relay;
return self;
}
- (instancetype)initWithIdentifier:(uint64_t)identifier
key:(NSData *)key
contentType:(NSString *)contentType
relay:(NSString *)relay
avatarOfGroupId:(NSData *)avatarOfGroupId {
self = [self initWithIdentifier:identifier key:key contentType:contentType relay:relay];
if (self) {
_relay = relay;
_avatarOfGroupId = avatarOfGroupId;
}
return self;
}
- (BOOL)isDownloaded {
return NO;
}
@end
NS_ASSUME_NONNULL_END

View File

@ -8,14 +8,15 @@
NS_ASSUME_NONNULL_BEGIN
@class TSAttachmentPointer;
@interface TSAttachmentStream : TSAttachment
@property (nonatomic) BOOL isDownloaded;
- (instancetype)init NS_UNAVAILABLE;
- (instancetype)initWithContentType:(NSString *)contentType NS_DESIGNATED_INITIALIZER;
- (instancetype)initWithPointer:(TSAttachmentPointer *)pointer NS_DESIGNATED_INITIALIZER;
- (instancetype)initWithIdentifier:(NSString *)identifier
data:(NSData *)data
key:(NSData *)key
contentType:(NSString *)contentType NS_DESIGNATED_INITIALIZER;
@property (atomic, readwrite) BOOL isDownloaded;
#if TARGET_OS_IPHONE
- (nullable UIImage *)image;
@ -26,6 +27,8 @@ NS_ASSUME_NONNULL_BEGIN
- (BOOL)isVideo;
- (nullable NSString *)filePath;
- (nullable NSURL *)mediaURL;
- (nullable NSData *)readDataFromFileWithError:(NSError **)error;
- (BOOL)writeData:(NSData *)data error:(NSError **)error;
+ (void)deleteAttachments;
+ (NSString *)attachmentsFolder;

View File

@ -3,6 +3,7 @@
#import "TSAttachmentStream.h"
#import "MIMETypeUtil.h"
#import "TSAttachmentPointer.h"
#import <AVFoundation/AVFoundation.h>
#import <YapDatabase/YapDatabaseTransaction.h>
@ -10,38 +11,52 @@ NS_ASSUME_NONNULL_BEGIN
@implementation TSAttachmentStream
- (instancetype)initWithIdentifier:(NSString *)identifier
data:(NSData *)data
key:(NSData *)key
contentType:(NSString *)contentType
- (instancetype)initWithContentType:(NSString *)contentType
{
self = [super initWithIdentifier:identifier encryptionKey:key contentType:contentType];
self = [super init];
if (!self) {
return self;
}
// TODO move this to save?
[[NSFileManager defaultManager] createFileAtPath:[self filePath] contents:data attributes:nil];
DDLogInfo(@"Created file at %@", [self filePath]);
_contentType = contentType;
_isDownloaded = YES;
return self;
}
- (instancetype)initWithPointer:(TSAttachmentPointer *)pointer
{
// Once saved, this AttachmentStream will replace the AttachmentPointer in the attachments collection.
self = [super initWithUniqueId:pointer.uniqueId];
if (!self) {
return self;
}
_contentType = pointer.contentType;
_isDownloaded = YES;
return self;
}
#pragma mark - TSYapDatabaseModel overrides
- (void)removeWithTransaction:(YapDatabaseReadWriteTransaction *)transaction
{
[super removeWithTransaction:transaction];
[self removeFile];
}
- (void)removeFile
{
NSError *error;
[[NSFileManager defaultManager] removeItemAtPath:[self filePath] error:&error];
#pragma mark - File Management
if (error) {
DDLogError(@"remove file errored with: %@", error);
}
- (nullable NSData *)readDataFromFileWithError:(NSError **)error
{
return [NSData dataWithContentsOfFile:self.filePath options:0 error:error];
}
- (BOOL)writeData:(NSData *)data error:(NSError **)error
{
DDLogInfo(@"%@ Created file at %@", self.tag, self.filePath);
return [data writeToFile:self.filePath options:0 error:error];
}
+ (NSString *)attachmentsFolder
@ -88,6 +103,16 @@ NS_ASSUME_NONNULL_BEGIN
return filePath ? [NSURL fileURLWithPath:filePath] : nil;
}
- (void)removeFile
{
NSError *error;
[[NSFileManager defaultManager] removeItemAtPath:[self filePath] error:&error];
if (error) {
DDLogError(@"%@ remove file errored with: %@", self.tag, error);
}
}
- (BOOL)isAnimated {
return [MIMETypeUtil isAnimated:self.contentType];
}
@ -134,6 +159,18 @@ NS_ASSUME_NONNULL_BEGIN
}
}
#pragma mark - Logging
+ (NSString *)tag
{
return [NSString stringWithFormat:@"[%@]", self.class];
}
- (NSString *)tag
{
return self.class.tag;
}
@end
NS_ASSUME_NONNULL_END

View File

@ -9,6 +9,8 @@ NS_ASSUME_NONNULL_BEGIN
* Abstract message class.
*/
@class TSAttachmentPointer;
typedef NS_ENUM(NSInteger, TSGroupMetaMessage) {
TSGroupMessageNone,
TSGroupMessageNew,

View File

@ -5,6 +5,7 @@
#import "NSDate+millisecondTimeStamp.h"
#import "OWSDisappearingMessagesJob.h"
#import "TSAttachment.h"
#import "TSAttachmentPointer.h"
#import "TSThread.h"
#import <YapDatabase/YapDatabaseTransaction.h>
@ -133,7 +134,6 @@ static const NSUInteger OWSMessageSchemaVersion = 3;
return self;
}
// Seconds.
- (void)setexpiresInSeconds:(uint32_t)expiresInSeconds
{
_expiresInSeconds = expiresInSeconds;

View File

@ -47,6 +47,9 @@ typedef NS_ENUM(NSInteger, TSOutgoingMessageState) {
@property (nonatomic) TSOutgoingMessageState messageState;
@property BOOL hasSyncedTranscript;
@property NSString *customMessage;
@property (atomic, readonly) NSString *mostRecentFailureText;
- (void)setSendingError:(NSError *)error;
/**
* Signal Identifier (e.g. e164 number) or nil if in a group thread.

View File

@ -89,6 +89,16 @@ NS_ASSUME_NONNULL_BEGIN
return self;
}
- (void)saveWithTransaction:(YapDatabaseReadWriteTransaction *)transaction
{
if (!(self.groupMetaMessage == TSGroupMessageDeliver || self.groupMetaMessage == TSGroupMessageNone)) {
DDLogDebug(@"%@ Skipping save for group meta message.", self.tag);
return;
}
[super saveWithTransaction:transaction];
}
- (nullable NSString *)recipientIdentifier
{
return self.thread.contactIdentifier;
@ -106,6 +116,11 @@ NS_ASSUME_NONNULL_BEGIN
}
}
- (void)setSendingError:(NSError *)error
{
_mostRecentFailureText = error.localizedDescription;
}
- (OWSSignalServiceProtosDataMessageBuilder *)dataMessageBuilder
{
TSThread *thread = self.thread;
@ -175,13 +190,25 @@ NS_ASSUME_NONNULL_BEGIN
TSAttachmentStream *attachmentStream = (TSAttachmentStream *)attachment;
OWSSignalServiceProtosAttachmentPointerBuilder *builder = [OWSSignalServiceProtosAttachmentPointerBuilder new];
[builder setId:[attachmentStream.identifier unsignedLongLongValue]];
[builder setId:attachmentStream.serverId];
[builder setContentType:attachmentStream.contentType];
[builder setKey:attachmentStream.encryptionKey];
return [builder build];
}
#pragma mark - Logging
+ (NSString *)tag
{
return [NSString stringWithFormat:@"[%@]", self.class];
}
- (NSString *)tag
{
return self.class.tag;
}
@end
NS_ASSUME_NONNULL_END

View File

@ -14,10 +14,10 @@ NS_ASSUME_NONNULL_BEGIN
@class TSOutgoingMessage;
@class TSThread;
@interface TSInvalidIdentityKeySendingErrorMessage : TSInvalidIdentityKeyErrorMessage
extern NSString *TSInvalidPreKeyBundleKey;
extern NSString *TSInvalidRecipientKey;
#define TSInvalidPreKeyBundleKey @"TSInvalidPreKeyBundleKey"
#define TSInvalidRecipientKey @"TSInvalidRecipientKey"
@interface TSInvalidIdentityKeySendingErrorMessage : TSInvalidIdentityKeyErrorMessage
+ (instancetype)untrustedKeyWithOutgoingMessage:(TSOutgoingMessage *)outgoingMessage
inThread:(TSThread *)thread
@ -26,6 +26,7 @@ NS_ASSUME_NONNULL_BEGIN
withTransaction:(YapDatabaseReadWriteTransaction *)transaction;
@property (nonatomic, readonly) NSString *recipientId;
@property (nonatomic, readonly) NSString *messageId;
@end

View File

@ -7,17 +7,18 @@
#import "SignalRecipient.h"
#import "TSContactThread.h"
#import "TSErrorMessage_privateConstructor.h"
#import "TSMessagesManager+sendMessages.h"
#import "TSOutgoingMessage.h"
#import "TSStorageManager+IdentityKeyStore.h"
#import <AxolotlKit/NSData+keyVersionByte.h>
NS_ASSUME_NONNULL_BEGIN
NSString *TSInvalidPreKeyBundleKey = @"TSInvalidPreKeyBundleKey";
NSString *TSInvalidRecipientKey = @"TSInvalidRecipientKey";
@interface TSInvalidIdentityKeySendingErrorMessage ()
@property (nonatomic, readonly) PreKeyBundle *preKeyBundle;
@property (nonatomic, readonly) NSString *messageId;
@end
@ -57,37 +58,6 @@ NS_ASSUME_NONNULL_BEGIN
- (void)acceptNewIdentityKey
{
[[TSStorageManager sharedManager] saveRemoteIdentity:self.newIdentityKey recipientId:self.recipientId];
__block TSOutgoingMessage *_Nullable message;
__block TSThread *thread;
__block SignalRecipient *recipient;
[[TSStorageManager sharedManager].newDatabaseConnection
readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
thread = [TSContactThread fetchObjectWithUniqueID:self.uniqueThreadId transaction:transaction];
message = [TSOutgoingMessage fetchObjectWithUniqueID:self.messageId transaction:transaction];
recipient = [SignalRecipient fetchObjectWithUniqueID:self.recipientId transaction:transaction];
[self removeWithTransaction:transaction];
}];
if (message) {
void (^logSuccess)() = ^void() {
DDLogInfo(@"Successfully redelivered message to recipient after accepting new key.");
};
void (^logFailure)() = ^void() {
DDLogWarn(@"Failed to redeliver message to recipient after accepting new key.");
};
// Resend to single recipient
[[TSMessagesManager sharedManager] resendMessage:message
toRecipient:recipient
inThread:thread
success:logSuccess
failure:logFailure];
}
}
- (NSData *)newIdentityKey

View File

@ -6,6 +6,7 @@ NS_ASSUME_NONNULL_BEGIN
@class TSStorageManager;
@class TSMessage;
@class TSThread;
@protocol ContactsManagerProtocol;
@interface OWSDisappearingMessagesJob : NSObject
@ -18,6 +19,21 @@ NS_ASSUME_NONNULL_BEGIN
- (void)setExpirationForMessage:(TSMessage *)message expirationStartedAt:(uint64_t)expirationStartedAt;
- (void)runBy:(uint64_t)millisecondTimestamp;
/**
* Synchronize our disappearing messages settings with that of the given message. Useful so we can
* become eventually consistent with remote senders.
*
* @param message
* Can be an expiring or non expiring message. We match the expiration timer of the message, including disabling
* expiring messages if the message is not an expiring message.
*
* @param contactsManager
* Provides the contact name responsible for any configuration changes in an info message.
*/
- (void)becomeConsistentWithConfigurationForMessage:(TSMessage *)message
contactsManager:(id<ContactsManagerProtocol>)contactsManager;
@end
NS_ASSUME_NONNULL_END

View File

@ -2,9 +2,12 @@
// Copyright © 2016 Open Whisper Systems. All rights reserved.
#import "OWSDisappearingMessagesJob.h"
#import "ContactsManagerProtocol.h"
#import "NSDate+millisecondTimeStamp.h"
#import "OWSDisappearingConfigurationUpdateInfoMessage.h"
#import "OWSDisappearingMessagesConfiguration.h"
#import "OWSDisappearingMessagesFinder.h"
#import "TSIncomingMessage.h"
#import "TSMessage.h"
NS_ASSUME_NONNULL_BEGIN
@ -154,6 +157,55 @@ NS_ASSUME_NONNULL_BEGIN
});
}
- (void)becomeConsistentWithConfigurationForMessage:(TSMessage *)message
contactsManager:(id<ContactsManagerProtocol>)contactsManager
{
// Become eventually consistent in the case that the remote changed their settings at the same time.
// Also in case remote doesn't support expiring messages
OWSDisappearingMessagesConfiguration *disappearingMessagesConfiguration =
[OWSDisappearingMessagesConfiguration fetchObjectWithUniqueID:message.uniqueThreadId];
BOOL changed = NO;
if (message.expiresInSeconds == 0) {
if (disappearingMessagesConfiguration.isEnabled) {
changed = YES;
DDLogWarn(@"%@ Received remote message which had no expiration set, disabling our expiration to become "
@"consistent.",
self.tag);
disappearingMessagesConfiguration.enabled = NO;
[disappearingMessagesConfiguration save];
}
} else if (message.expiresInSeconds != disappearingMessagesConfiguration.durationSeconds) {
changed = YES;
DDLogInfo(
@"%@ Received remote message with different expiration set, updating our expiration to become consistent.",
self.tag);
disappearingMessagesConfiguration.enabled = YES;
disappearingMessagesConfiguration.durationSeconds = message.expiresInSeconds;
[disappearingMessagesConfiguration save];
}
if (!changed) {
return;
}
if ([message isKindOfClass:[TSIncomingMessage class]]) {
TSIncomingMessage *incomingMessage = (TSIncomingMessage *)message;
NSString *contactName = [contactsManager nameStringForPhoneIdentifier:incomingMessage.authorId];
[[[OWSDisappearingConfigurationUpdateInfoMessage alloc] initWithTimestamp:message.timestamp
thread:message.thread
configuration:disappearingMessagesConfiguration
createdByRemoteName:contactName] save];
} else {
[[[OWSDisappearingConfigurationUpdateInfoMessage alloc] initWithTimestamp:message.timestamp
thread:message.thread
configuration:disappearingMessagesConfiguration]
save];
}
}
#pragma mark - Logging
+ (NSString *)tag

View File

@ -4,13 +4,13 @@
NS_ASSUME_NONNULL_BEGIN
@class TSStorageManager;
@class TSMessagesManager;
@class OWSMessageSender;
@interface OWSIncomingMessageReadObserver : NSObject
- (instancetype)init NS_UNAVAILABLE;
- (instancetype)initWithStorageManager:(TSStorageManager *)storageManager
messagesManager:(TSMessagesManager *)messagesManager NS_DESIGNATED_INITIALIZER;
messageSender:(OWSMessageSender *)messageSender NS_DESIGNATED_INITIALIZER;
- (void)startObserving;

View File

@ -26,7 +26,7 @@ NS_ASSUME_NONNULL_BEGIN
}
- (instancetype)initWithStorageManager:(TSStorageManager *)storageManager
messagesManager:(TSMessagesManager *)messagesManager;
messageSender:(OWSMessageSender *)messageSender
{
self = [super init];
if (!self) {
@ -35,7 +35,7 @@ NS_ASSUME_NONNULL_BEGIN
_isObserving = NO;
_disappearingMessagesJob = [[OWSDisappearingMessagesJob alloc] initWithStorageManager:storageManager];
_sendReadReceiptsJob = [[OWSSendReadReceiptsJob alloc] initWithMessagesManager:messagesManager];
_sendReadReceiptsJob = [[OWSSendReadReceiptsJob alloc] initWithMessageSender:messageSender];
return self;
}

View File

@ -3,21 +3,75 @@
NS_ASSUME_NONNULL_BEGIN
@class TSOutgoingMessage;
@class TSNetworkManager;
@class TSStorageManager;
@class ContactsUpdater;
@class OWSUploadingService;
@class SignalRecipient;
@class TSInvalidIdentityKeySendingErrorMessage;
@class TSNetworkManager;
@class TSOutgoingMessage;
@class TSStorageManager;
@class TSThread;
@protocol ContactsManagerProtocol;
@interface OWSMessageSender : NSObject
@interface OWSMessageSender : NSObject {
- (instancetype)initWithMessage:(TSOutgoingMessage *)message
networkManager:(TSNetworkManager *)networkManager
storageManager:(TSStorageManager *)storageManager
contactsManager:(id<ContactsManagerProtocol>)contactsManager
contactsUpdater:(ContactsUpdater *)contactsUpdater;
@protected
- (void)sendWithSuccess:(void (^)())successBlock failure:(void (^)(NSError *error))failureBlock;
// For subclassing in tests
OWSUploadingService *_uploadingService;
ContactsUpdater *_contactsUpdater;
}
- (instancetype)initWithNetworkManager:(TSNetworkManager *)networkManager
storageManager:(TSStorageManager *)storageManager
contactsManager:(id<ContactsManagerProtocol>)contactsManager
contactsUpdater:(ContactsUpdater *)contactsUpdater;
/**
* Send and resend text messages or resend messages with existing attachments.
* If you haven't yet created the attachment, see the `sendAttachmentData:` variants.
*/
- (void)sendMessage:(TSOutgoingMessage *)message
success:(void (^)())successHandler
failure:(void (^)(NSError *error))failureHandler;
/**
* Takes care of allocating and uploading the attachment, then sends the message.
* Only necessary to call once. If sending fails, retry with `sendMessage:`.
*/
- (void)sendAttachmentData:(NSData *)attachmentData
contentType:(NSString *)contentType
inMessage:(TSOutgoingMessage *)outgoingMessage
success:(void (^)())successHandler
failure:(void (^)(NSError *error))failureHandler;
/**
* Same as `sendAttachmentData:`, but deletes the local copy of the attachment after sending.
* Used for sending sync request data, not for user visible attachments.
*/
- (void)sendTemporaryAttachmentData:(NSData *)attachmentData
contentType:(NSString *)contentType
inMessage:(TSOutgoingMessage *)outgoingMessage
success:(void (^)())successHandler
failure:(void (^)(NSError *error))failureHandler;
/**
* Resend a message to a select recipient in a thread when previous sending failed due to key error.
* e.g. If a key change prevents one recipient from receiving the message, we don't want to resend to the entire group.
*/
- (void)resendMessageFromKeyError:(TSInvalidIdentityKeySendingErrorMessage *)errorMessage
success:(void (^)())successHandler
failure:(void (^)(NSError *error))failureHandler;
- (void)handleMessageSentRemotely:(TSOutgoingMessage *)message sentAt:(uint64_t)sentAt;
/**
* Set local configuration to match that of the of `outgoingMessage`'s sender
*
* We do this because messages and async message latency make it possible for thread participants disappearing messags
* configuration to get out of sync.
*/
- (void)becomeConsistentWithDisappearingConfigurationForMessage:(TSOutgoingMessage *)outgoingMessage;
@end

View File

@ -2,57 +2,789 @@
// Copyright © 2016 Open Whisper Systems. All rights reserved.
#import "OWSMessageSender.h"
#import "ContactsUpdater.h"
#import "NSData+messagePadding.h"
#import "OWSDisappearingMessagesJob.h"
#import "OWSDispatch.h"
#import "OWSError.h"
#import "TSMessagesManager+sendMessages.h"
#import "OWSLegacyMessageServiceParams.h"
#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"
#import "TSNetworkManager.h"
#import "TSOutgoingMessage.h"
#import "TSStorageManager+IdentityKeyStore.h"
#import "TSStorageManager+PreKeyStore.h"
#import "TSStorageManager+SignedPreKeyStore.h"
#import "TSStorageManager+keyingMaterial.h"
#import "TSStorageManager+sessionStore.h"
#import "TSStorageManager.h"
#import "TSThread.h"
#import <AxolotlKit/AxolotlExceptions.h>
#import <AxolotlKit/CipherMessage.h>
#import <AxolotlKit/PreKeyBundle.h>
#import <AxolotlKit/SessionBuilder.h>
#import <AxolotlKit/SessionCipher.h>
#import <TwistedOakCollapsingFutures/CollapsingFutures.h>
NS_ASSUME_NONNULL_BEGIN
int const OWSMessageSenderRetryAttempts = 3;
NSString *const OWSMessageSenderInvalidDeviceException = @"InvalidDeviceException";
@interface OWSMessageSender ()
@property (nonatomic, readonly) TSOutgoingMessage *message;
@property (nonatomic, readonly) TSNetworkManager *networkManager;
@property (nonatomic, readonly) TSMessagesManager *messagesManager;
@property (nonatomic, readonly) TSStorageManager *storageManager;
@property (nonatomic, readonly) OWSUploadingService *uploadingService;
@property (nonatomic, readonly) YapDatabaseConnection *dbConnection;
@property (nonatomic, readonly) id<ContactsManagerProtocol> contactsManager;
@property (nonatomic, readonly) ContactsUpdater *contactsUpdater;
@property (nonatomic, readonly) OWSDisappearingMessagesJob *disappearingMessagesJob;
@end
@implementation OWSMessageSender
- (instancetype)initWithMessage:(TSOutgoingMessage *)message
networkManager:(TSNetworkManager *)networkManager
storageManager:(TSStorageManager *)storageManager
contactsManager:(id<ContactsManagerProtocol>)contactsManager
contactsUpdater:(ContactsUpdater *)contactsUpdater
- (instancetype)initWithNetworkManager:(TSNetworkManager *)networkManager
storageManager:(TSStorageManager *)storageManager
contactsManager:(id<ContactsManagerProtocol>)contactsManager
contactsUpdater:(ContactsUpdater *)contactsUpdater
{
self = [super init];
if (!self) {
return self;
}
_message = message;
_networkManager = networkManager;
_messagesManager = [[TSMessagesManager alloc] initWithNetworkManager:networkManager
storageManager:storageManager
contactsManager:contactsManager
contactsUpdater:contactsUpdater];
_storageManager = storageManager;
_contactsManager = contactsManager;
_contactsUpdater = contactsUpdater;
_uploadingService = [[OWSUploadingService alloc] initWithNetworkManager:networkManager];
_dbConnection = storageManager.newDatabaseConnection;
_disappearingMessagesJob = [[OWSDisappearingMessagesJob alloc] initWithStorageManager:storageManager];
return self;
}
- (void)sendWithSuccess:(void (^)())successBlock failure:(void (^)(NSError *error))failureBlock
- (void)sendMessage:(TSOutgoingMessage *)message
success:(void (^)())successHandler
failure:(void (^)(NSError *error))failureHandler
{
[self.messagesManager sendMessage:self.message
inThread:self.message.thread
success:^{
successBlock();
void (^markAndFailureHandler)(NSError *error) = ^(NSError *error) {
[self saveMessage:message withError:error];
failureHandler(error);
};
[self ensureAnyAttachmentsUploaded:message
success:^() {
[self deliverMessage:message success:successHandler failure:markAndFailureHandler];
}
failure:markAndFailureHandler];
}
- (void)ensureAnyAttachmentsUploaded:(TSOutgoingMessage *)message
success:(void (^)())successHandler
failure:(void (^)(NSError *error))failureHandler
{
if (!message.hasAttachments) {
DDLogDebug(@"%@ No attachments for message: %@", self.tag, message);
return successHandler();
}
TSAttachmentStream *attachmentStream =
[TSAttachmentStream fetchObjectWithUniqueID:message.attachmentIds.firstObject];
if (!attachmentStream) {
DDLogError(@"%@ Unable to find local saved attachment to upload.", self.tag);
NSError *error = OWSErrorMakeFailedToSendOutgoingMessageError();
return failureHandler(error);
}
[self.uploadingService uploadAttachmentStream:attachmentStream
message:message
success:successHandler
failure:failureHandler];
}
- (void)sendTemporaryAttachmentData:(NSData *)attachmentData
contentType:(NSString *)contentType
inMessage:(TSOutgoingMessage *)message
success:(void (^)())successHandler
failure:(void (^)(NSError *error))failureHandler
{
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];
};
[self sendAttachmentData:attachmentData
contentType:contentType
inMessage:message
success:successWithDeleteHandler
failure:failureWithDeleteHandler];
}
- (void)sendAttachmentData:(NSData *)data
contentType:(NSString *)contentType
inMessage:(TSOutgoingMessage *)message
success:(void (^)())successHandler
failure:(void (^)(NSError *error))failureHandler
{
dispatch_async([OWSDispatch attachmentsQueue], ^{
TSAttachmentStream *attachmentStream = [[TSAttachmentStream alloc] initWithContentType:contentType];
NSError *error;
[attachmentStream writeData:data error:&error];
if (error) {
DDLogError(@"%@ Failed to write data for outgoing attachment with error:%@", self.tag, error);
return failureHandler(error);
}
failure:^{
NSString *localizedError
= NSLocalizedString(@"NOTIFICATION_SEND_FAILED", @"Generic notice when message failed to send.");
NSError *error = OWSErrorWithCodeDescription(OWSErrorCodeFailedToSendOutgoingMessage, localizedError);
failureBlock(error);
[attachmentStream save];
[message.attachmentIds addObject:attachmentStream.uniqueId];
message.messageState = TSOutgoingMessageStateAttemptingOut;
[message save];
[self sendMessage:message success:successHandler failure:failureHandler];
});
}
- (void)resendMessageFromKeyError:(TSInvalidIdentityKeySendingErrorMessage *)errorMessage
success:(void (^)())successHandler
failure:(void (^)(NSError *error))failureHandler
{
TSOutgoingMessage *message = [TSOutgoingMessage fetchObjectWithUniqueID:errorMessage.messageId];
// Here we remove the existing error message because sending a new message will either
// 1.) succeed and create a new successful message in the thread or...
// 2.) fail and create a new identical error message in the thread.
[errorMessage remove];
if ([errorMessage.thread isKindOfClass:[TSContactThread class]]) {
return [self sendMessage:message success:successHandler failure:failureHandler];
}
// else it's a GroupThread
dispatch_async([OWSDispatch sendingQueue], ^{
// Avoid spamming entire group when resending failed message.
SignalRecipient *failedRecipient = [SignalRecipient fetchObjectWithUniqueID:errorMessage.recipientId];
// Normally marking as unsent is handled in sendMessage happy path, but beacuse we're skipping the common entry
// point to message sending in order to send to a single recipient, we have to handle it ourselves.
void (^markAndFailureHandler)(NSError *error) = ^(NSError *error) {
[self saveMessage:message withError:error];
failureHandler(error);
};
[self groupSend:@[ failedRecipient ]
message:message
thread:message.thread
success:successHandler
failure:markAndFailureHandler];
});
}
- (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.
DDLogError(@"%@ Unknown error finding contacts", self.tag);
*error = OWSErrorMakeFailedToSendOutgoingMessageError();
}
return [recipients copy];
}
- (void)deliverMessage:(TSOutgoingMessage *)message
success:(void (^)())successHandler
failure:(void (^)(NSError *error))failureHandler
{
TSThread *thread = message.thread;
dispatch_async([OWSDispatch sendingQueue], ^{
if ([thread isKindOfClass:[TSGroupThread class]]) {
TSGroupThread *gThread = (TSGroupThread *)thread;
NSError *error;
NSArray<SignalRecipient *> *recipients =
[self getRecipients:gThread.groupModel.groupMemberIds error:&error];
if (error) {
return failureHandler(error);
}
[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;
[self saveMessage:message withState:TSOutgoingMessageStateAttemptingOut];
if ([contactThread.contactIdentifier isEqualToString:self.storageManager.localNumber]
&& ![message isKindOfClass:[OWSOutgoingSyncMessage class]]) {
[self handleSendToMyself:message];
return;
}
NSString *recipientContactId = [message isKindOfClass:[OWSOutgoingSyncMessage class]]
? self.storageManager.localNumber
: contactThread.contactIdentifier;
__block SignalRecipient *recipient = [SignalRecipient recipientWithTextSecureIdentifier:recipientContactId];
if (!recipient) {
NSError *error;
// possibly returns nil.
recipient = [self.contactsUpdater synchronousLookup:contactThread.contactIdentifier error:&error];
if (error) {
if (error.code == NOTFOUND_ERROR) {
DDLogWarn(@"recipient contact not found with error: %@", error);
[self unregisteredRecipient:recipient message:message thread:thread];
}
DDLogError(@"contact lookup failed with error: %@", error);
return failureHandler(error);
}
}
if (!recipient) {
NSError *error = OWSErrorMakeFailedToSendOutgoingMessageError();
DDLogWarn(@"recipient contact still not found after attempting lookup.");
return failureHandler(error);
}
[self sendMessage:message
recipient:recipient
thread:thread
attempts:OWSMessageSenderRetryAttempts
success:successHandler
failure:failureHandler];
}
});
}
/// For group sends, we're using chained futures to make the code more readable.
- (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:^{
[futureSource trySetResult:@1];
}
failure:^(NSError *error) {
[futureSource trySetFailure:error];
}];
return futureSource.future;
}
- (void)groupSend:(NSArray<SignalRecipient *> *)recipients
message:(TSOutgoingMessage *)message
thread:(TSThread *)thread
success:(void (^)())successHandler
failure:(void (^)(NSError *error))failureHandler
{
[self saveGroupMessage:message inThread:thread];
NSMutableArray<TOCFuture *> *futures = [NSMutableArray array];
for (SignalRecipient *rec in recipients) {
// we don't need to send the message to ourselves, but otherwise we send
if (![[rec uniqueId] isEqualToString:[TSStorageManager localNumber]]) {
[futures addObject:[self sendMessageFuture:message recipient:rec thread:thread]];
}
}
TOCFuture *completionFuture = futures.toc_thenAll;
[completionFuture thenDo:^(id value) {
successHandler();
}];
[completionFuture catchDo:^(id failure) {
if ([failure isKindOfClass:[NSError class]]) {
return failureHandler(failure);
} else if ([failure isKindOfClass:[NSArray class]]) {
NSArray *errors = (NSArray *)failure;
// failure from toc_thenAll is a Future, rather than the future's failure.
if ([errors.lastObject isKindOfClass:[TOCFuture class]]) {
TOCFuture *lastFailure = (TOCFuture *)errors.lastObject;
id failureResult = lastFailure.forceGetFailure;
if ([failureResult isKindOfClass:[NSError class]]) {
return failureHandler((NSError *)failureResult);
}
}
}
DDLogWarn(@"%@ Unexpected generic failure: %@", self.tag, failure);
return failureHandler(OWSErrorMakeFailedToSendOutgoingMessageError());
}];
}
- (void)unregisteredRecipient:(SignalRecipient *)recipient
message:(TSOutgoingMessage *)message
thread:(TSThread *)thread
{
[self.dbConnection asyncReadWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
[recipient removeWithTransaction:transaction];
[[TSInfoMessage userNotRegisteredMessageInThread:thread transaction:transaction]
saveWithTransaction:transaction];
}];
}
- (void)sendMessage:(TSOutgoingMessage *)message
recipient:(SignalRecipient *)recipient
thread:(TSThread *)thread
attempts:(int)remainingAttempts
success:(void (^)())successHandler
failure:(void (^)(NSError *error))failureHandler
{
if (remainingAttempts <= 0) {
// We should always fail with a specific error.
DDLogError(@"%@ Unexpected generic failure.", self.tag);
return failureHandler(OWSErrorMakeFailedToSendOutgoingMessageError());
}
remainingAttempts -= 1;
NSArray<NSDictionary *> *deviceMessages;
@try {
deviceMessages = [self deviceMessages:message forRecipient:recipient inThread:thread];
} @catch (NSException *exception) {
deviceMessages = @[];
if (remainingAttempts == 0) {
DDLogWarn(
@"%@ Terminal failure to build any device messages. Giving up with exception:%@", self.tag, exception);
[self processException:exception outgoingMessage:message inThread:thread];
NSError *error = OWSErrorMakeFailedToSendOutgoingMessageError();
failureHandler(error);
}
}
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) {
NSHTTPURLResponse *response = (NSHTTPURLResponse *)task.response;
long statuscode = response.statusCode;
NSData *responseData = error.userInfo[AFNetworkingOperationFailingURLResponseDataErrorKey];
void (^retrySend)() = ^void() {
if (remainingAttempts <= 0) {
return failureHandler(error);
}
dispatch_async([OWSDispatch sendingQueue], ^{
[self sendMessage:message
recipient:recipient
thread:thread
attempts:remainingAttempts
success:successHandler
failure:failureHandler];
});
};
switch (statuscode) {
case 404: {
[self unregisteredRecipient:recipient message:message thread:thread];
NSError *error = OWSErrorMakeFailedToSendOutgoingMessageError();
return failureHandler(error);
}
case 409: {
// Mismatched devices
DDLogWarn(@"%@ Mismatch Devices.", self.tag);
NSError *error;
NSDictionary *serializedResponse =
[NSJSONSerialization JSONObjectWithData:responseData options:0 error:&error];
if (error) {
DDLogError(@"%@ Failed to serialize response of mismatched devices: %@", self.tag, error);
return failureHandler(error);
}
[self handleMismatchedDevices:serializedResponse recipient:recipient];
retrySend();
break;
}
case 410: {
// staledevices
DDLogWarn(@"Stale devices");
if (!responseData) {
DDLogWarn(@"Stale devices but server didn't specify devices in response.");
NSError *error = OWSErrorMakeUnableToProcessServerResponseError();
return failureHandler(error);
}
[self handleStaleDevicesWithResponse:responseData recipientId:recipient.uniqueId];
retrySend();
break;
}
default:
retrySend();
break;
}
}];
}
- (void)handleMismatchedDevices:(NSDictionary *)dictionary recipient:(SignalRecipient *)recipient
{
NSArray *extraDevices = [dictionary objectForKey:@"extraDevices"];
NSArray *missingDevices = [dictionary objectForKey:@"missingDevices"];
if (extraDevices && extraDevices.count > 0) {
for (NSNumber *extraDeviceId in extraDevices) {
[self.storageManager deleteSessionForContact:recipient.uniqueId deviceId:extraDeviceId.intValue];
}
[recipient removeDevices:[NSSet setWithArray:extraDevices]];
}
if (missingDevices && missingDevices.count > 0) {
[recipient addDevices:[NSSet setWithArray:missingDevices]];
}
[recipient save];
}
- (void)handleMessageSentLocally:(TSOutgoingMessage *)message
{
[self saveMessage:message withState:TSOutgoingMessageStateSent];
if (message.shouldSyncTranscript) {
message.hasSyncedTranscript = YES;
[self sendSyncTranscriptForMessage:message];
}
[self.disappearingMessagesJob setExpirationForMessage:message];
}
- (void)handleMessageSentRemotely:(TSOutgoingMessage *)message sentAt:(uint64_t)sentAt
{
[self saveMessage:message withState:TSOutgoingMessageStateDelivered];
[self becomeConsistentWithDisappearingConfigurationForMessage:message];
[self.disappearingMessagesJob setExpirationForMessage:message expirationStartedAt:sentAt];
}
- (void)becomeConsistentWithDisappearingConfigurationForMessage:(TSOutgoingMessage *)outgoingMessage
{
[self.disappearingMessagesJob becomeConsistentWithConfigurationForMessage:outgoingMessage
contactsManager:self.contactsManager];
}
- (void)handleSendToMyself:(TSOutgoingMessage *)outgoingMessage
{
[self.dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
TSContactThread *cThread =
[TSContactThread getOrCreateThreadWithContactId:[TSAccountManager localNumber] transaction:transaction];
[cThread saveWithTransaction:transaction];
TSIncomingMessage *incomingMessage =
[[TSIncomingMessage alloc] initWithTimestamp:(outgoingMessage.timestamp + 1)
inThread:cThread
authorId:[cThread contactIdentifier]
messageBody:outgoingMessage.body
attachmentIds:outgoingMessage.attachmentIds
expiresInSeconds:outgoingMessage.expiresInSeconds];
[incomingMessage saveWithTransaction:transaction];
}];
[self handleMessageSentLocally:outgoingMessage];
}
- (void)sendSyncTranscriptForMessage:(TSOutgoingMessage *)message
{
OWSOutgoingSentMessageTranscript *sentMessageTranscript =
[[OWSOutgoingSentMessageTranscript alloc] initWithOutgoingMessage:message];
[self sendMessage:sentMessageTranscript
recipient:[SignalRecipient selfRecipient]
thread:message.thread
attempts:OWSMessageSenderRetryAttempts
success:^{
DDLogInfo(@"Succesfully sent sync transcript.");
}
failure:^(NSError *error) {
DDLogInfo(@"Failed to send sync transcript:%@", error);
}];
}
- (NSArray<NSDictionary *> *)deviceMessages:(TSOutgoingMessage *)message
forRecipient:(SignalRecipient *)recipient
inThread:(TSThread *)thread
{
NSMutableArray *messagesArray = [NSMutableArray arrayWithCapacity:recipient.devices.count];
NSData *plainText = [message buildPlainTextData];
for (NSNumber *deviceNumber in recipient.devices) {
@try {
// DEPRECATED - Remove after all clients have been upgraded.
BOOL isLegacyMessage = ![message isKindOfClass:[OWSOutgoingSyncMessage class]];
NSDictionary *messageDict = [self encryptedMessageWithPlaintext:plainText
toRecipient:recipient.uniqueId
deviceId:deviceNumber
keyingStorage:[TSStorageManager sharedManager]
legacy:isLegacyMessage];
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
legacy:(BOOL)isLegacymessage
{
if (![storage containsSession:identifier deviceId:[deviceNumber intValue]]) {
__block dispatch_semaphore_t sema = dispatch_semaphore_create(0);
__block PreKeyBundle *bundle;
[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) {
DDLogError(@"Server replied on PreKeyBundle request with error: %@", error);
NSHTTPURLResponse *response = (NSHTTPURLResponse *)task.response;
if (response.statusCode == 404) {
@throw [NSException exceptionWithName:OWSMessageSenderInvalidDeviceException
reason:@"Device not registered"
userInfo:nil];
}
dispatch_semaphore_signal(sema);
}];
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
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
identityKeyStore:storage
recipientId:identifier
deviceId:[deviceNumber intValue]];
@try {
[builder processPrekeyBundle:bundle];
} @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
identityKeyStore:storage
recipientId:identifier
deviceId:[deviceNumber intValue]];
id<CipherMessage> encryptedMessage = [cipher encryptMessage:[plainText paddedMessageBody]];
NSData *serializedMessage = encryptedMessage.serialized;
TSWhisperMessageType messageType = [self messageTypeForCipherMessage:encryptedMessage];
OWSMessageServiceParams *messageParams;
// DEPRECATED - Remove after all clients have been upgraded.
if (isLegacymessage) {
messageParams = [[OWSLegacyMessageServiceParams alloc] initWithType:messageType
recipientId:identifier
device:[deviceNumber intValue]
body:serializedMessage
registrationId:cipher.remoteRegistrationId];
} else {
messageParams = [[OWSMessageServiceParams alloc] initWithType:messageType
recipientId:identifier
device:[deviceNumber intValue]
content:serializedMessage
registrationId:cipher.remoteRegistrationId];
}
NSError *error;
NSDictionary *jsonDict = [MTLJSONAdapter JSONDictionaryFromModel:messageParams error:&error];
if (error) {
DDLogError(@"Error while making JSON dictionary of message: %@", error.debugDescription);
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)saveMessage:(TSOutgoingMessage *)message withState:(TSOutgoingMessageState)state
{
message.messageState = state;
[message save];
}
- (void)saveMessage:(TSOutgoingMessage *)message withError:(NSError *)error
{
message.messageState = TSOutgoingMessageStateUnsent;
[message setSendingError:error];
[message save];
}
- (void)saveGroupMessage:(TSOutgoingMessage *)message inThread:(TSThread *)thread
{
if (message.groupMetaMessage == TSGroupMessageDeliver) {
[self saveMessage:message withState:message.messageState];
} else if (message.groupMetaMessage == TSGroupMessageQuit) {
[[[TSInfoMessage alloc] initWithTimestamp:message.timestamp
inThread:thread
messageType:TSInfoMessageTypeGroupQuit
customMessage:message.customMessage] save];
} else {
[[[TSInfoMessage alloc] initWithTimestamp:message.timestamp
inThread:thread
messageType:TSInfoMessageTypeGroupUpdate
customMessage:message.customMessage] save];
}
}
- (void)handleStaleDevicesWithResponse:(NSData *)responseData recipientId:(NSString *)identifier
{
dispatch_async([OWSDispatch sendingQueue], ^{
NSDictionary *serialization = [NSJSONSerialization JSONObjectWithData:responseData options:0 error:nil];
NSArray *devices = serialization[@"staleDevices"];
if (!([devices count] > 0)) {
return;
}
for (NSUInteger i = 0; i < [devices count]; i++) {
int deviceNumber = [devices[i] intValue];
[[TSStorageManager sharedManager] deleteSessionForContact:identifier deviceId:deviceNumber];
}
});
}
- (void)processException:(NSException *)exception
outgoingMessage:(TSOutgoingMessage *)message
inThread:(TSThread *)thread
{
DDLogWarn(@"%@ Got exception: %@", self.tag, exception);
[self.dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
TSErrorMessage *errorMessage;
if ([exception.name isEqualToString:UntrustedIdentityKeyException]) {
errorMessage = [TSInvalidIdentityKeySendingErrorMessage
untrustedKeyWithOutgoingMessage:message
inThread:thread
forRecipient:exception.userInfo[TSInvalidRecipientKey]
preKeyBundle:exception.userInfo[TSInvalidPreKeyBundleKey]
withTransaction:transaction];
message.messageState = TSOutgoingMessageStateUnsent;
[message saveWithTransaction:transaction];
} else if (message.groupMetaMessage == TSGroupMessageNone) {
// Only update this with exception if it is not a group message as group
// messages may except for one group
// send but not another and the UI doesn't know how to handle that
[message setMessageState:TSOutgoingMessageStateUnsent];
[message saveWithTransaction:transaction];
}
[errorMessage saveWithTransaction:transaction];
}];
}
#pragma mark - Logging
+ (NSString *)tag
{
return [NSString stringWithFormat:@"[%@]", self.class];
}
- (NSString *)tag
{
return self.class.tag;
}
@end

View File

@ -4,7 +4,7 @@
NS_ASSUME_NONNULL_BEGIN
@class OWSDisappearingMessagesConfiguration;
@class TSMessagesManager;
@class OWSMessageSender;
@class TSThread;
@interface OWSNotifyRemoteOfUpdatedDisappearingConfigurationJob : NSObject
@ -12,11 +12,11 @@ NS_ASSUME_NONNULL_BEGIN
- (instancetype)init NS_UNAVAILABLE;
- (instancetype)initWithConfiguration:(OWSDisappearingMessagesConfiguration *)configuration
thread:(TSThread *)thread
messagesManager:(TSMessagesManager *)messagesManager NS_DESIGNATED_INITIALIZER;
messageSender:(OWSMessageSender *)messageSender NS_DESIGNATED_INITIALIZER;
+ (void)runWithConfiguration:(OWSDisappearingMessagesConfiguration *)configuration
thread:(TSThread *)thread
messagesManager:(TSMessagesManager *)messagesManager;
messageSender:(OWSMessageSender *)messageSender;
- (void)run;

View File

@ -3,14 +3,14 @@
#import "OWSNotifyRemoteOfUpdatedDisappearingConfigurationJob.h"
#import "OWSDisappearingMessagesConfigurationMessage.h"
#import "TSMessagesManager+sendMessages.h"
#import "OWSMessageSender.h"
NS_ASSUME_NONNULL_BEGIN
@interface OWSNotifyRemoteOfUpdatedDisappearingConfigurationJob ()
@property (nonatomic, readonly) OWSDisappearingMessagesConfiguration *configuration;
@property (nonatomic, readonly) TSMessagesManager *messageManager;
@property (nonatomic, readonly) OWSMessageSender *messageSender;
@property (nonatomic, readonly) TSThread *thread;
@end
@ -19,7 +19,7 @@ NS_ASSUME_NONNULL_BEGIN
- (instancetype)initWithConfiguration:(OWSDisappearingMessagesConfiguration *)configuration
thread:(TSThread *)thread
messagesManager:(TSMessagesManager *)messagesManager
messageSender:(OWSMessageSender *)messageSender
{
self = [super init];
if (!self) {
@ -28,17 +28,17 @@ NS_ASSUME_NONNULL_BEGIN
_thread = thread;
_configuration = configuration;
_messageManager = messagesManager;
_messageSender = messageSender;
return self;
}
+ (void)runWithConfiguration:(OWSDisappearingMessagesConfiguration *)configuration
thread:(TSThread *)thread
messagesManager:(TSMessagesManager *)messagesManager
messageSender:(OWSMessageSender *)messageSender
{
OWSNotifyRemoteOfUpdatedDisappearingConfigurationJob *job =
[[self alloc] initWithConfiguration:configuration thread:thread messagesManager:messagesManager];
[[self alloc] initWithConfiguration:configuration thread:thread messageSender:messageSender];
[job run];
}
@ -48,14 +48,16 @@ NS_ASSUME_NONNULL_BEGIN
[[OWSDisappearingMessagesConfigurationMessage alloc] initWithConfiguration:self.configuration
thread:self.thread];
[self.messageManager sendMessage:message
inThread:self.thread
[self.messageSender sendMessage:message
success:^{
DDLogDebug(
@"%@ Successfully notified %@ of new disappearing messages configuration", self.tag, self.thread);
}
failure:^{
DDLogError(@"%@ Failed to notify %@ of new disappearing messages configuration", self.tag, self.thread);
failure:^(NSError *error) {
DDLogError(@"%@ Failed to notify %@ of new disappearing messages configuration with error: %@",
self.tag,
self.thread,
error);
}];
}

View File

@ -23,5 +23,4 @@
- (NSString *)getInfoStringAboutUpdateTo:(TSGroupModel *)model contactsManager:(id<ContactsManagerProtocol>)contactsManager;
#endif
@end

View File

@ -1,40 +0,0 @@
//
// TSMessagesManager+attachments.h
// Signal
//
// Created by Frederic Jacobs on 17/12/14.
// Copyright (c) 2014 Open Whisper Systems. All rights reserved.
//
#import "TSMessagesManager+sendMessages.h"
#import "TSMessagesManager.h"
@class TSAttachment;
@class TSAttachmentPointer;
@interface TSMessagesManager (attachments)
- (void)handleReceivedMediaWithEnvelope:(OWSSignalServiceProtosEnvelope *)envelope
dataMessage:(OWSSignalServiceProtosDataMessage *)message;
- (void)sendAttachment:(NSData *)attachmentData
contentType:(NSString *)contentType
inMessage:(TSOutgoingMessage *)outgoingMessage
thread:(TSThread *)thread
success:(successSendingCompletionBlock)successCompletionBlock
failure:(failedSendingCompletionBlock)failedCompletionBlock;
/**
* Delete the local copy of the attachment after sending. Used for sending sync request data, not for user visible
* attachments.
*/
- (void)sendTemporaryAttachment:(NSData *)attachmentData
contentType:(NSString *)contentType
inMessage:(TSOutgoingMessage *)outgoingMessage
thread:(TSThread *)thread
success:(successSendingCompletionBlock)successCompletionBlock
failure:(failedSendingCompletionBlock)failedCompletionBlock;
- (void)retrieveAttachment:(TSAttachmentPointer *)attachment messageId:(NSString *)messageId;
@end

View File

@ -1,345 +0,0 @@
// Created by Frederic Jacobs on 17/12/14.
// Copyright (c) 2014 Open Whisper Systems. All rights reserved.
#import "Cryptography.h"
#import "MIMETypeUtil.h"
#import "NSDate+millisecondTimeStamp.h"
#import "OWSAttachmentsProcessor.h"
#import "TSAttachmentPointer.h"
#import "TSContactThread.h"
#import "TSGroupModel.h"
#import "TSGroupThread.h"
#import "TSInfoMessage.h"
#import "TSMessagesManager+attachments.h"
#import "TSNetworkManager.h"
#import <YapDatabase/YapDatabaseConnection.h>
#import <YapDatabase/YapDatabaseTransaction.h>
@interface TSMessagesManager ()
dispatch_queue_t attachmentsQueue(void);
@end
dispatch_queue_t attachmentsQueue() {
static dispatch_once_t queueCreationGuard;
static dispatch_queue_t queue;
dispatch_once(&queueCreationGuard, ^{
queue = dispatch_queue_create("org.whispersystems.signal.attachments", NULL);
});
return queue;
}
@implementation TSMessagesManager (attachments)
- (void)handleReceivedMediaWithEnvelope:(OWSSignalServiceProtosEnvelope *)envelope
dataMessage:(OWSSignalServiceProtosDataMessage *)dataMessage
{
NSData *avatarGroupId;
NSArray<OWSSignalServiceProtosAttachmentPointer *> *attachmentPointerProtos;
if (dataMessage.hasGroup && (dataMessage.group.type == OWSSignalServiceProtosGroupContextTypeUpdate)) {
avatarGroupId = dataMessage.group.id;
attachmentPointerProtos = @[ dataMessage.group.avatar ];
} else {
attachmentPointerProtos = dataMessage.attachments;
}
TSThread *thread = [self threadForEnvelope:envelope dataMessage:dataMessage];
OWSAttachmentsProcessor *attachmentsProcessor = [[OWSAttachmentsProcessor alloc]
initWithAttachmentPointersProtos:attachmentPointerProtos
timestamp:envelope.timestamp
relay:envelope.relay
avatarGroupId:avatarGroupId
inThread:thread
messagesManager:[TSMessagesManager sharedManager]]; // TODO self?
if (attachmentsProcessor.hasSupportedAttachments) {
TSIncomingMessage *possiblyCreatedMessage =
[self handleReceivedEnvelope:envelope
withDataMessage:dataMessage
attachmentIds:attachmentsProcessor.supportedAttachmentIds];
[attachmentsProcessor fetchAttachmentsForMessageId:possiblyCreatedMessage.uniqueId];
}
}
- (void)sendTemporaryAttachment:(NSData *)attachmentData
contentType:(NSString *)contentType
inMessage:(TSOutgoingMessage *)outgoingMessage
thread:(TSThread *)thread
success:(successSendingCompletionBlock)successCompletionBlock
failure:(failedSendingCompletionBlock)failedCompletionBlock
{
void (^successBlockWithDelete)() = ^{
if (successCompletionBlock) {
successCompletionBlock();
}
DDLogDebug(@"Removing temporary attachment message.");
[outgoingMessage remove];
};
void (^failureBlockWithDelete)() = ^{
if (failedCompletionBlock) {
failedCompletionBlock();
}
DDLogDebug(@"Removing temporary attachment message.");
[outgoingMessage remove];
};
[self sendAttachment:attachmentData
contentType:contentType
inMessage:outgoingMessage
thread:thread
success:successBlockWithDelete
failure:failureBlockWithDelete];
}
- (void)sendAttachment:(NSData *)attachmentData
contentType:(NSString *)contentType
inMessage:(TSOutgoingMessage *)outgoingMessage
thread:(TSThread *)thread
success:(successSendingCompletionBlock)successCompletionBlock
failure:(failedSendingCompletionBlock)failedCompletionBlock {
TSRequest *allocateAttachment = [[TSAllocAttachmentRequest alloc] init];
[self.networkManager makeRequest:allocateAttachment
success:^(NSURLSessionDataTask *task, id responseObject) {
dispatch_async(attachmentsQueue(), ^{
if ([responseObject isKindOfClass:[NSDictionary class]]) {
NSDictionary *responseDict = (NSDictionary *)responseObject;
NSString *attachmentId = [(NSNumber *)[responseDict objectForKey:@"id"] stringValue];
NSString *location = [responseDict objectForKey:@"location"];
TSAttachmentEncryptionResult *result =
[Cryptography encryptAttachment:attachmentData contentType:contentType identifier:attachmentId];
result.pointer.isDownloaded = NO;
[result.pointer save];
outgoingMessage.body = nil;
[outgoingMessage.attachmentIds addObject:attachmentId];
if (outgoingMessage.groupMetaMessage != TSGroupMessageNew &&
outgoingMessage.groupMetaMessage != TSGroupMessageUpdate) {
[outgoingMessage setMessageState:TSOutgoingMessageStateAttemptingOut];
[outgoingMessage save];
}
BOOL success = [self uploadDataWithProgress:result.body location:location attachmentID:attachmentId];
if (success) {
result.pointer.isDownloaded = YES;
[result.pointer save];
[self sendMessage:outgoingMessage
inThread:thread
success:^{
if (successCompletionBlock) {
successCompletionBlock();
}
}
failure:^{
if (failedCompletionBlock) {
failedCompletionBlock();
}
}];
} else {
if (failedCompletionBlock) {
failedCompletionBlock();
}
DDLogWarn(@"Failed to upload attachment");
}
} else {
if (failedCompletionBlock) {
failedCompletionBlock();
}
DDLogError(@"The server didn't returned an empty responseObject");
}
});
}
failure:^(NSURLSessionDataTask *task, NSError *error) {
if (failedCompletionBlock) {
failedCompletionBlock();
}
DDLogError(@"Failed to get attachment allocated: %@", error);
}];
}
- (void)sendAttachment:(NSData *)attachmentData
contentType:(NSString *)contentType
thread:(TSThread *)thread
success:(successSendingCompletionBlock)successCompletionBlock
failure:(failedSendingCompletionBlock)failedCompletionBlock
{
TSOutgoingMessage *message = [[TSOutgoingMessage alloc] initWithTimestamp:[NSDate ows_millisecondTimeStamp]
inThread:thread
messageBody:nil
attachmentIds:[NSMutableArray new]];
[self sendAttachment:attachmentData
contentType:contentType
inMessage:message
thread:thread
success:successCompletionBlock
failure:failedCompletionBlock];
}
- (void)retrieveAttachment:(TSAttachmentPointer *)attachment messageId:(NSString *)messageId {
[self setAttachment:attachment isDownloadingInMessage:messageId];
TSAttachmentRequest *attachmentRequest =
[[TSAttachmentRequest alloc] initWithId:[attachment identifier] relay:attachment.relay];
[self.networkManager makeRequest:attachmentRequest
success:^(NSURLSessionDataTask *task, id responseObject) {
if ([responseObject isKindOfClass:[NSDictionary class]]) {
dispatch_async(attachmentsQueue(), ^{
NSString *location = [(NSDictionary *)responseObject objectForKey:@"location"];
NSData *data = [self downloadFromLocation:location pointer:attachment messageId:messageId];
if (data) {
[self decryptedAndSaveAttachment:attachment data:data messageId:messageId];
}
});
} else {
DDLogError(@"Failed retrieval of attachment. Response had unexpected format.");
[self setFailedAttachment:attachment inMessage:messageId];
}
}
failure:^(NSURLSessionDataTask *task, NSError *error) {
DDLogError(@"Failed retrieval of attachment with error: %@", error.description);
[self setFailedAttachment:attachment inMessage:messageId];
}];
}
- (void)setAttachment:(TSAttachmentPointer *)pointer isDownloadingInMessage:(NSString *)messageId {
[self.dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
[pointer setDownloading:YES];
[pointer saveWithTransaction:transaction];
TSMessage *message = [TSMessage fetchObjectWithUniqueID:messageId transaction:transaction];
[message saveWithTransaction:transaction];
}];
}
- (void)setFailedAttachment:(TSAttachmentPointer *)pointer inMessage:(NSString *)messageId {
[self.dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
[pointer setDownloading:NO];
[pointer setFailed:YES];
[pointer saveWithTransaction:transaction];
TSMessage *message = [TSMessage fetchObjectWithUniqueID:messageId transaction:transaction];
[message saveWithTransaction:transaction];
}];
}
- (void)decryptedAndSaveAttachment:(TSAttachmentPointer *)attachment
data:(NSData *)cipherText
messageId:(NSString *)messageId
{
NSData *plaintext = [Cryptography decryptAttachment:cipherText withKey:attachment.encryptionKey];
if (!plaintext) {
DDLogError(@"Failed to get attachment decrypted ...");
} else {
TSAttachmentStream *stream = [[TSAttachmentStream alloc] initWithIdentifier:attachment.uniqueId
data:plaintext
key:attachment.encryptionKey
contentType:attachment.contentType];
[self.dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
[stream saveWithTransaction:transaction];
if ([attachment.avatarOfGroupId length] != 0) {
TSGroupModel *emptyModelToFillOutId =
[[TSGroupModel alloc] initWithTitle:nil memberIds:nil image:nil groupId:attachment.avatarOfGroupId];
TSGroupThread *gThread =
[TSGroupThread getOrCreateThreadWithGroupModel:emptyModelToFillOutId transaction:transaction];
gThread.groupModel.groupImage = [stream image];
// Avatars are stored directly in the database, so there's no need to keep the attachment around after
// assigning the image.
[stream removeWithTransaction:transaction];
[[TSMessage fetchObjectWithUniqueID:messageId] saveWithTransaction:transaction];
[gThread saveWithTransaction:transaction];
} else {
// Causing message to be reloaded in view.
TSMessage *message = [TSMessage fetchObjectWithUniqueID:messageId transaction:transaction];
[message saveWithTransaction:transaction];
}
}];
}
}
- (NSData *)downloadFromLocation:(NSString *)location
pointer:(TSAttachmentPointer *)pointer
messageId:(NSString *)messageId {
__block NSData *data;
AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
manager.requestSerializer = [AFHTTPRequestSerializer serializer];
[manager.requestSerializer setValue:OWSMimeTypeApplicationOctetStream forHTTPHeaderField:@"Content-Type"];
manager.responseSerializer = [AFHTTPResponseSerializer serializer];
manager.completionQueue = dispatch_get_main_queue();
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
[manager GET:location
parameters:nil
progress:nil
success:^(NSURLSessionDataTask *_Nonnull task, id _Nullable responseObject) {
data = responseObject;
dispatch_semaphore_signal(sema);
}
failure:^(NSURLSessionDataTask *_Nullable task, NSError *_Nonnull error) {
DDLogError(@"Failed to retrieve attachment with error: %@", error.description);
if (pointer && messageId) {
[self setFailedAttachment:pointer inMessage:messageId];
}
dispatch_semaphore_signal(sema);
}];
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
return data;
}
- (BOOL)uploadDataWithProgress:(NSData *)cipherText
location:(NSString *)location
attachmentID:(NSString *)attachmentID {
// AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
// manager.responseSerializer = [AFHTTPResponseSerializer serializer];
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
__block BOOL success = NO;
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:[NSURL URLWithString:location]];
request.HTTPMethod = @"PUT";
request.HTTPBody = cipherText;
[request setValue:OWSMimeTypeApplicationOctetStream forHTTPHeaderField:@"Content-Type"];
AFURLSessionManager *manager = [[AFURLSessionManager alloc]
initWithSessionConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]];
NSURLSessionUploadTask *uploadTask;
uploadTask = [manager uploadTaskWithRequest:request
fromData:cipherText
progress:^(NSProgress *_Nonnull uploadProgress) {
NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];
[notificationCenter postNotificationName:@"attachmentUploadProgress"
object:nil
userInfo:@{
@"progress" : @(uploadProgress.fractionCompleted),
@"attachmentID" : attachmentID
}];
}
completionHandler:^(NSURLResponse *_Nonnull response, id _Nullable responseObject, NSError *_Nullable error) {
NSInteger statusCode = ((NSHTTPURLResponse *)response).statusCode;
BOOL isValidResponse = (statusCode >= 200) && (statusCode < 400);
if (!error && isValidResponse) {
success = YES;
dispatch_semaphore_signal(sema);
} else {
DDLogError(@"Failed uploading attachment with error: %@", error.description);
success = NO;
dispatch_semaphore_signal(sema);
}
}];
[uploadTask resume];
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
return success;
}
@end

View File

@ -1,31 +0,0 @@
//
// TSMessagesManager+sendMessages.h
// TextSecureKit
//
// Created by Frederic Jacobs on 17/11/14.
// Copyright (c) 2014 Open Whisper Systems. All rights reserved.
//
#import "TSMessagesManager.h"
@class SignalRecipient;
@interface TSMessagesManager (sendMessages)
typedef void (^successSendingCompletionBlock)();
typedef void (^failedSendingCompletionBlock)();
- (void)sendMessage:(TSOutgoingMessage *)message
inThread:(TSThread *)thread
success:(successSendingCompletionBlock)successCompletionBlock
failure:(failedSendingCompletionBlock)failedCompletionBlock;
- (void)resendMessage:(TSOutgoingMessage *)message
toRecipient:(SignalRecipient *)recipient
inThread:(TSThread *)thread
success:(successSendingCompletionBlock)successCompletionBlock
failure:(failedSendingCompletionBlock)failedCompletionBlock;
- (void)handleMessageSentRemotely:(TSOutgoingMessage *)message sentAt:(uint64_t)sentAt;
@end

View File

@ -1,626 +0,0 @@
// Created by Frederic Jacobs on 17/11/14.
// Copyright (c) 2014 Open Whisper Systems. All rights reserved.
#import "ContactsUpdater.h"
#import "NSData+messagePadding.h"
#import "NSDate+millisecondTimeStamp.h"
#import "OWSDisappearingMessagesConfiguration.h"
#import "OWSDisappearingMessagesJob.h"
#import "OWSLegacyMessageServiceParams.h"
#import "OWSMessageServiceParams.h"
#import "OWSOutgoingSentMessageTranscript.h"
#import "PreKeyBundle+jsonDict.h"
#import "TSAccountManager.h"
#import "TSAttachmentStream.h"
#import "TSContactThread.h"
#import "TSGroupThread.h"
#import "TSInfoMessage.h"
#import "TSMessagesManager+sendMessages.h"
#import "TSNetworkManager.h"
#import "TSStorageHeaders.h"
#import <AxolotlKit/AxolotlExceptions.h>
#import <AxolotlKit/SessionBuilder.h>
#import <AxolotlKit/SessionCipher.h>
#import <Mantle/Mantle.h>
#import <TwistedOakCollapsingFutures/CollapsingFutures.h>
#define RETRY_ATTEMPTS 3
#define InvalidDeviceException @"InvalidDeviceException"
@interface TSMessagesManager (sendMessagesPrivate)
dispatch_queue_t sendingQueue(void);
@property TSNetworkManager *networkManager;
@end
typedef void (^messagesQueue)(NSArray *messages);
@implementation TSMessagesManager (sendMessages)
dispatch_queue_t sendingQueue() {
static dispatch_once_t queueCreationGuard;
static dispatch_queue_t queue;
dispatch_once(&queueCreationGuard, ^{
queue = dispatch_queue_create("org.whispersystems.signal.sendQueue", NULL);
});
return queue;
}
- (void)getRecipients:(NSArray<NSString *> *)identifiers
success:(void (^)(NSArray<SignalRecipient *> *))success
failure:(void (^)(NSError *error))failure {
NSMutableArray<SignalRecipient *> *recipients = [NSMutableArray array];
__block NSError *latestError;
for (NSString *recipientId in identifiers) {
__block SignalRecipient *recipient;
[self.dbConnection readWithBlock:^(YapDatabaseReadTransaction *_Nonnull transaction) {
recipient = [SignalRecipient recipientWithTextSecureIdentifier:recipientId withTransaction:transaction];
}];
if (!recipient) {
[self.contactsUpdater synchronousLookup:recipientId
success:^(SignalRecipient *newRecipient) {
[recipients addObject:newRecipient];
}
failure:^(NSError *error) {
DDLogWarn(@"Not sending message to unknown recipient with error: %@", error);
latestError = error;
}];
} else {
[recipients addObject:recipient];
}
}
if (recipients > 0) {
success(recipients);
} else {
failure(latestError);
}
return;
}
- (void)resendMessage:(TSOutgoingMessage *)message
toRecipient:(SignalRecipient *)recipient
inThread:(TSThread *)thread
success:(successSendingCompletionBlock)successCompletionBlock
failure:(failedSendingCompletionBlock)failedCompletionBlock
{
if ([thread isKindOfClass:[TSGroupThread class]]) {
dispatch_async(sendingQueue(), ^{
[self groupSend:@[ recipient ] // Avoid spamming entire group when resending failed message.
Message:message
inThread:thread
success:successCompletionBlock
failure:failedCompletionBlock];
});
} else {
[self sendMessage:message inThread:thread success:successCompletionBlock failure:failedCompletionBlock];
}
}
- (void)sendMessage:(TSOutgoingMessage *)message
inThread:(TSThread *)thread
success:(successSendingCompletionBlock)successCompletionBlock
failure:(failedSendingCompletionBlock)failedCompletionBlock {
dispatch_async(sendingQueue(), ^{
if ([thread isKindOfClass:[TSGroupThread class]]) {
TSGroupThread *groupThread = (TSGroupThread *)thread;
[self getRecipients:groupThread.groupModel.groupMemberIds
success:^(NSArray<SignalRecipient *> *recipients) {
[self groupSend:recipients
Message:message
inThread:thread
success:successCompletionBlock
failure:failedCompletionBlock];
}
failure:^(NSError *error) {
DDLogError(@"Failure to retreive group recipient.");
[self saveMessage:message withState:TSOutgoingMessageStateUnsent];
}];
} else if ([thread isKindOfClass:[TSContactThread class]] ||
[message isKindOfClass:[OWSOutgoingSyncMessage class]]) {
TSContactThread *contactThread = (TSContactThread *)thread;
[self saveMessage:message withState:TSOutgoingMessageStateAttemptingOut];
if (![contactThread.contactIdentifier isEqualToString:[TSAccountManager localNumber]] ||
[message isKindOfClass:[OWSOutgoingSyncMessage class]]) {
NSString *recipientContactId = [message isKindOfClass:[OWSOutgoingSyncMessage class]]
? [TSAccountManager localNumber]
: contactThread.contactIdentifier;
__block SignalRecipient *recipient =
[SignalRecipient recipientWithTextSecureIdentifier:recipientContactId];
if (!recipient) {
[self.contactsUpdater synchronousLookup:contactThread.contactIdentifier
success:^(SignalRecipient *recip) {
recipient = recip;
}
failure:^(NSError *error) {
if (error.code == NOTFOUND_ERROR) {
DDLogWarn(@"recipient contact not found with error: %@", error);
[self unregisteredRecipient:recipient message:message inThread:thread];
return;
} else {
DDLogError(@"contact lookup failed with error: %@", error);
[self saveMessage:message withState:TSOutgoingMessageStateUnsent];
return;
}
}];
}
if (recipient) {
[self sendMessage:message
toRecipient:recipient
inThread:thread
withAttemps:RETRY_ATTEMPTS
success:successCompletionBlock
failure:failedCompletionBlock];
}
} else {
// Special situation: if we are sending to ourselves in a single thread, we treat this as an incoming
// message
[self handleSendToMyself:message];
}
}
});
}
/// For group sends, we're using chained futures to make the code more readable.
- (TOCFuture *)sendMessageFuture:(TSOutgoingMessage *)message
recipient:(SignalRecipient *)recipient
inThread:(TSThread *)thread {
TOCFutureSource *futureSource = [[TOCFutureSource alloc] init];
[self sendMessage:message
toRecipient:recipient
inThread:thread
withAttemps:RETRY_ATTEMPTS
success:^{
[futureSource trySetResult:@1];
}
failure:^{
[futureSource trySetFailure:@0];
}];
return futureSource.future;
}
- (void)groupSend:(NSArray<SignalRecipient *> *)recipients
Message:(TSOutgoingMessage *)message
inThread:(TSThread *)thread
success:(successSendingCompletionBlock)successBlock
failure:(failedSendingCompletionBlock)failureBlock
{
[self saveGroupMessage:message inThread:thread];
NSMutableArray<TOCFuture *> *futures = [NSMutableArray array];
for (SignalRecipient *rec in recipients) {
// we don't need to send the message to ourselves, but otherwise we send
if (![[rec uniqueId] isEqualToString:[TSStorageManager localNumber]]) {
[futures addObject:[self sendMessageFuture:message recipient:rec inThread:thread]];
}
}
TOCFuture *completionFuture = futures.toc_thenAll;
[completionFuture thenDo:^(id value) {
BLOCK_SAFE_RUN(successBlock);
}];
[completionFuture catchDo:^(id failure) {
BLOCK_SAFE_RUN(failureBlock);
}];
}
- (void)unregisteredRecipient:(SignalRecipient *)recipient
message:(TSOutgoingMessage *)message
inThread:(TSThread *)thread {
[self.dbConnection asyncReadWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
[recipient removeWithTransaction:transaction];
[[TSInfoMessage userNotRegisteredMessageInThread:thread transaction:transaction] saveWithTransaction:transaction];
}];
[self saveMessage:message withState:TSOutgoingMessageStateUnsent];
}
- (void)sendMessage:(TSOutgoingMessage *)message
toRecipient:(SignalRecipient *)recipient
inThread:(TSThread *)thread
withAttemps:(int)remainingAttempts
success:(successSendingCompletionBlock)successBlock
failure:(failedSendingCompletionBlock)failureBlock
{
if (remainingAttempts > 0) {
remainingAttempts -= 1;
NSArray<NSDictionary *> *deviceMessages;
@try {
deviceMessages = [self deviceMessages:message forRecipient:recipient inThread:thread];
} @catch (NSException *exception) {
deviceMessages = @[];
if (remainingAttempts == 0) {
DDLogWarn(@"%@ Terminal failure to build any device messages. Giving up with exception:%@",
self.logTag,
exception);
[self processException:exception outgoingMessage:message inThread:thread];
BLOCK_SAFE_RUN(failureBlock);
}
}
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(sendingQueue(), ^{
[recipient save];
[self handleMessageSentLocally:message];
BLOCK_SAFE_RUN(successBlock);
});
}
failure:^(NSURLSessionDataTask *task, NSError *error) {
NSHTTPURLResponse *response = (NSHTTPURLResponse *)task.response;
long statuscode = response.statusCode;
NSData *responseData = error.userInfo[AFNetworkingOperationFailingURLResponseDataErrorKey];
switch (statuscode) {
case 404: {
[self unregisteredRecipient:recipient message:message inThread:thread];
BLOCK_SAFE_RUN(failureBlock);
break;
}
case 409: {
// Mismatched devices
DDLogWarn(@"%@ Mismatch Devices.", self.logTag);
NSError *e;
NSDictionary *serializedResponse =
[NSJSONSerialization JSONObjectWithData:responseData options:0 error:&e];
if (e) {
DDLogError(@"%@ Failed to serialize response of mismatched devices: %@", self.logTag, e);
} else {
[self handleMismatchedDevices:serializedResponse recipient:recipient];
}
dispatch_async(sendingQueue(), ^{
[self sendMessage:message
toRecipient:recipient
inThread:thread
withAttemps:remainingAttempts
success:successBlock
failure:failureBlock];
});
break;
}
case 410: {
// staledevices
DDLogWarn(@"Stale devices");
if (!responseData) {
DDLogWarn(@"Stale devices but server didn't specify devices in response.");
BLOCK_SAFE_RUN(failureBlock);
return;
}
[self handleStaleDevicesWithResponse:responseData recipientId:recipient.uniqueId];
dispatch_async(sendingQueue(), ^{
[self sendMessage:message
toRecipient:recipient
inThread:thread
withAttemps:remainingAttempts
success:successBlock
failure:failureBlock];
});
break;
}
default:
[self sendMessage:message
toRecipient:recipient
inThread:thread
withAttemps:remainingAttempts
success:successBlock
failure:failureBlock];
break;
}
}];
} else {
[self saveMessage:message withState:TSOutgoingMessageStateUnsent];
BLOCK_SAFE_RUN(failureBlock);
}
}
- (void)handleMismatchedDevices:(NSDictionary *)dictionary recipient:(SignalRecipient *)recipient {
NSArray *extraDevices = [dictionary objectForKey:@"extraDevices"];
NSArray *missingDevices = [dictionary objectForKey:@"missingDevices"];
if (extraDevices && [extraDevices count] > 0) {
for (NSNumber *extraDeviceId in extraDevices) {
[[TSStorageManager sharedManager] deleteSessionForContact:recipient.uniqueId
deviceId:[extraDeviceId intValue]];
}
[recipient removeDevices:[NSSet setWithArray:extraDevices]];
}
if (missingDevices && [missingDevices count] > 0) {
[recipient addDevices:[NSSet setWithArray:missingDevices]];
}
[self.dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
[recipient saveWithTransaction:transaction];
}];
}
- (void)handleMessageSentLocally:(TSOutgoingMessage *)message
{
[self saveMessage:message withState:TSOutgoingMessageStateSent];
if (message.shouldSyncTranscript) {
message.hasSyncedTranscript = YES;
[self sendSyncTranscriptForMessage:message];
}
[self.disappearingMessagesJob setExpirationForMessage:message];
}
- (void)handleMessageSentRemotely:(TSOutgoingMessage *)message sentAt:(uint64_t)sentAt
{
[self saveMessage:message withState:TSOutgoingMessageStateDelivered];
[self becomeConsistentWithDisappearingConfigurationForMessage:message];
[self.disappearingMessagesJob setExpirationForMessage:message expirationStartedAt:sentAt];
}
- (void)handleSendToMyself:(TSOutgoingMessage *)outgoingMessage
{
[self handleMessageSentLocally:outgoingMessage];
if (!(outgoingMessage.body || outgoingMessage.hasAttachments)) {
DDLogDebug(
@"%@ Refusing to make incoming copy of non-standard message sent to self:%@", self.logTag, outgoingMessage);
return;
}
[self.dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
TSContactThread *cThread =
[TSContactThread getOrCreateThreadWithContactId:[TSAccountManager localNumber] transaction:transaction];
[cThread saveWithTransaction:transaction];
TSIncomingMessage *incomingMessage =
[[TSIncomingMessage alloc] initWithTimestamp:(outgoingMessage.timestamp + 1)
inThread:cThread
authorId:[cThread contactIdentifier]
messageBody:outgoingMessage.body
attachmentIds:outgoingMessage.attachmentIds
expiresInSeconds:outgoingMessage.expiresInSeconds];
[incomingMessage saveWithTransaction:transaction];
}];
}
- (void)sendSyncTranscriptForMessage:(TSOutgoingMessage *)message
{
OWSOutgoingSentMessageTranscript *sentMessageTranscript =
[[OWSOutgoingSentMessageTranscript alloc] initWithOutgoingMessage:message];
[self sendMessage:sentMessageTranscript
toRecipient:[SignalRecipient selfRecipient]
inThread:message.thread
withAttemps:RETRY_ATTEMPTS
success:^{
DDLogInfo(@"Succesfully sent sync transcript.");
}
failure:^{
DDLogInfo(@"Failed to send sync transcript.");
}];
}
- (NSArray<NSDictionary *> *)deviceMessages:(TSOutgoingMessage *)message
forRecipient:(SignalRecipient *)recipient
inThread:(TSThread *)thread
{
NSMutableArray *messagesArray = [NSMutableArray arrayWithCapacity:recipient.devices.count];
NSData *plainText = [message buildPlainTextData];
for (NSNumber *deviceNumber in recipient.devices) {
@try {
// DEPRECATED - Remove after all clients have been upgraded.
BOOL isLegacyMessage = ![message isKindOfClass:[OWSOutgoingSyncMessage class]];
NSDictionary *messageDict = [self encryptedMessageWithPlaintext:plainText
toRecipient:recipient.uniqueId
deviceId:deviceNumber
keyingStorage:[TSStorageManager sharedManager]
legacy:isLegacyMessage];
if (messageDict) {
[messagesArray addObject:messageDict];
} else {
@throw [NSException exceptionWithName:InvalidMessageException
reason:@"Failed to encrypt message"
userInfo:nil];
}
} @catch (NSException *exception) {
if ([exception.name isEqualToString:InvalidDeviceException]) {
[recipient removeDevices:[NSSet setWithObject:deviceNumber]];
} else {
@throw exception;
}
}
}
return [messagesArray copy];
}
- (NSDictionary *)encryptedMessageWithPlaintext:(NSData *)plainText
toRecipient:(NSString *)identifier
deviceId:(NSNumber *)deviceNumber
keyingStorage:(TSStorageManager *)storage
legacy:(BOOL)isLegacymessage
{
if (![storage containsSession:identifier deviceId:[deviceNumber intValue]]) {
__block dispatch_semaphore_t sema = dispatch_semaphore_create(0);
__block PreKeyBundle *bundle;
[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) {
DDLogError(@"Server replied on PreKeyBundle request with error: %@", error);
NSHTTPURLResponse *response = (NSHTTPURLResponse *)task.response;
if (response.statusCode == 404) {
@throw [NSException exceptionWithName:InvalidDeviceException
reason:@"Device not registered"
userInfo:nil];
}
dispatch_semaphore_signal(sema);
}];
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
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
identityKeyStore:storage
recipientId:identifier
deviceId:[deviceNumber intValue]];
@try {
[builder processPrekeyBundle:bundle];
} @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
identityKeyStore:storage
recipientId:identifier
deviceId:[deviceNumber intValue]];
id<CipherMessage> encryptedMessage = [cipher encryptMessage:[plainText paddedMessageBody]];
NSData *serializedMessage = encryptedMessage.serialized;
TSWhisperMessageType messageType = [self messageTypeForCipherMessage:encryptedMessage];
OWSMessageServiceParams *messageParams;
// DEPRECATED - Remove after all clients have been upgraded.
if (isLegacymessage) {
messageParams = [[OWSLegacyMessageServiceParams alloc] initWithType:messageType
recipientId:identifier
device:[deviceNumber intValue]
body:serializedMessage
registrationId:cipher.remoteRegistrationId];
} else {
messageParams = [[OWSMessageServiceParams alloc] initWithType:messageType
recipientId:identifier
device:[deviceNumber intValue]
content:serializedMessage
registrationId:cipher.remoteRegistrationId];
}
NSError *error;
NSDictionary *jsonDict = [MTLJSONAdapter JSONDictionaryFromModel:messageParams error:&error];
if (error) {
DDLogError(@"Error while making JSON dictionary of message: %@", error.debugDescription);
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)saveMessage:(TSOutgoingMessage *)message withState:(TSOutgoingMessageState)state {
if (message.groupMetaMessage == TSGroupMessageDeliver || message.groupMetaMessage == TSGroupMessageNone) {
[self.dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
[message setMessageState:state];
[message saveWithTransaction:transaction];
}];
}
}
- (void)saveGroupMessage:(TSOutgoingMessage *)message inThread:(TSThread *)thread {
if (message.groupMetaMessage == TSGroupMessageDeliver) {
[self saveMessage:message withState:message.messageState];
} else if (message.groupMetaMessage == TSGroupMessageQuit) {
[self.dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
[[[TSInfoMessage alloc] initWithTimestamp:message.timestamp
inThread:thread
messageType:TSInfoMessageTypeGroupQuit
customMessage:message.customMessage] saveWithTransaction:transaction];
}];
} else {
[self.dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
[[[TSInfoMessage alloc] initWithTimestamp:message.timestamp
inThread:thread
messageType:TSInfoMessageTypeGroupUpdate
customMessage:message.customMessage] saveWithTransaction:transaction];
}];
}
}
- (void)handleStaleDevicesWithResponse:(NSData *)responseData recipientId:(NSString *)identifier {
dispatch_async(sendingQueue(), ^{
NSDictionary *serialization = [NSJSONSerialization JSONObjectWithData:responseData options:0 error:nil];
NSArray *devices = serialization[@"staleDevices"];
if (!([devices count] > 0)) {
return;
}
for (NSUInteger i = 0; i < [devices count]; i++) {
int deviceNumber = [devices[i] intValue];
[[TSStorageManager sharedManager] deleteSessionForContact:identifier deviceId:deviceNumber];
}
});
}
+ (NSString *)logTag
{
return [NSString stringWithFormat:@"[%@]", self.class];
}
- (NSString *)logTag
{
return self.class.logTag;
}
@end

View File

@ -13,28 +13,26 @@ NS_ASSUME_NONNULL_BEGIN
@class OWSSignalServiceProtosDataMessage;
@class ContactsUpdater;
@class OWSDisappearingMessagesJob;
@class OWSMessageSender;
@protocol ContactsManagerProtocol;
@interface TSMessagesManager : NSObject
- (instancetype)init NS_UNAVAILABLE;
- (instancetype)initWithNetworkManager:(TSNetworkManager *)networkManager
storageManager:(TSStorageManager *)storageManager
contactsManager:(id<ContactsManagerProtocol>)contactsManager
contactsUpdater:(ContactsUpdater *)contactsUpdater NS_DESIGNATED_INITIALIZER;
contactsUpdater:(ContactsUpdater *)contactsUpdater
messageSender:(OWSMessageSender *)messageSender NS_DESIGNATED_INITIALIZER;
+ (instancetype)sharedManager;
@property (nonatomic, readonly) YapDatabaseConnection *dbConnection;
@property (nonatomic, readonly) TSNetworkManager *networkManager;
@property (nonatomic, readonly) ContactsUpdater *contactsUpdater;
@property (nonatomic, readonly) OWSDisappearingMessagesJob *disappearingMessagesJob;
- (void)handleReceivedEnvelope:(OWSSignalServiceProtosEnvelope *)envelope;
- (void)processException:(NSException *)exception
outgoingMessage:(TSOutgoingMessage *)message
inThread:(TSThread *)thread;
/**
* Processes all kinds of incoming envelopes with a data message, along with any attachments.
*
@ -53,16 +51,6 @@ NS_ASSUME_NONNULL_BEGIN
- (TSThread *)threadForEnvelope:(OWSSignalServiceProtosEnvelope *)envelope
dataMessage:(OWSSignalServiceProtosDataMessage *)dataMessage;
/**
* Synchronize our disappearing messages settings with that of the given message. Useful so we can
* become eventually consistent with remote senders.
*
* @param message
* Can be an expiring or non expiring message. We match the expiration timer of the message, including disabling
* expiring messages if the message is not an expiring message.
*/
- (void)becomeConsistentWithDisappearingConfigurationForMessage:(TSMessage *)message;
- (NSUInteger)unreadMessagesCount;
- (NSUInteger)unreadMessagesCountExcept:(TSThread *)thread;
- (NSUInteger)unreadMessagesInThread:(TSThread *)thread;

View File

@ -7,10 +7,12 @@
#import "MimeTypeUtil.h"
#import "NSData+messagePadding.h"
#import "NSDate+millisecondTimeStamp.h"
#import "OWSAttachmentsProcessor.h"
#import "OWSDisappearingConfigurationUpdateInfoMessage.h"
#import "OWSDisappearingMessagesConfiguration.h"
#import "OWSDisappearingMessagesJob.h"
#import "OWSIncomingSentMessageTranscript.h"
#import "OWSMessageSender.h"
#import "OWSReadReceiptsProcessor.h"
#import "OWSRecordTranscriptJob.h"
#import "OWSSyncContactsMessage.h"
@ -24,7 +26,6 @@
#import "TSGroupThread.h"
#import "TSInfoMessage.h"
#import "TSInvalidIdentityKeyReceivingErrorMessage.h"
#import "TSMessagesManager+attachments.h"
#import "TSNetworkManager.h"
#import "TSStorageHeaders.h"
#import "TextSecureKitEnv.h"
@ -37,6 +38,8 @@ NS_ASSUME_NONNULL_BEGIN
@property (nonatomic, readonly) id<ContactsManagerProtocol> contactsManager;
@property (nonatomic, readonly) TSStorageManager *storageManager;
@property (nonatomic, readonly) OWSMessageSender *messageSender;
@property (nonatomic, readonly) OWSDisappearingMessagesJob *disappearingMessagesJob;
@end
@ -46,23 +49,34 @@ NS_ASSUME_NONNULL_BEGIN
static TSMessagesManager *sharedMyManager = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedMyManager = [[self alloc] init];
sharedMyManager = [[self alloc] initDefault];
});
return sharedMyManager;
}
- (instancetype)init
- (instancetype)initDefault
{
return [self initWithNetworkManager:[TSNetworkManager sharedManager]
storageManager:[TSStorageManager sharedManager]
contactsManager:[TextSecureKitEnv sharedEnv].contactsManager
contactsUpdater:[ContactsUpdater sharedUpdater]];
TSNetworkManager *networkManager = [TSNetworkManager sharedManager];
TSStorageManager *storageManager = [TSStorageManager sharedManager];
id<ContactsManagerProtocol> contactsManager = [TextSecureKitEnv sharedEnv].contactsManager;
ContactsUpdater *contactsUpdater = [ContactsUpdater sharedUpdater];
OWSMessageSender *messageSender = [[OWSMessageSender alloc] initWithNetworkManager:networkManager
storageManager:storageManager
contactsManager:contactsManager
contactsUpdater:contactsUpdater];
return [self initWithNetworkManager:networkManager
storageManager:storageManager
contactsManager:contactsManager
contactsUpdater:contactsUpdater
messageSender:messageSender];
}
- (instancetype)initWithNetworkManager:(TSNetworkManager *)networkManager
storageManager:(TSStorageManager *)storageManager
contactsManager:(id<ContactsManagerProtocol>)contactsManager
contactsUpdater:(ContactsUpdater *)contactsUpdater
messageSender:(OWSMessageSender *)messageSender
{
self = [super init];
@ -74,6 +88,7 @@ NS_ASSUME_NONNULL_BEGIN
_networkManager = networkManager;
_contactsManager = contactsManager;
_contactsUpdater = contactsUpdater;
_messageSender = messageSender;
_dbConnection = storageManager.newDatabaseConnection;
_disappearingMessagesJob = [[OWSDisappearingMessagesJob alloc] initWithStorageManager:storageManager];
@ -81,6 +96,8 @@ NS_ASSUME_NONNULL_BEGIN
return self;
}
#pragma mark - message handling
- (void)handleReceivedEnvelope:(OWSSignalServiceProtosEnvelope *)envelope
{
@try {
@ -255,18 +272,76 @@ NS_ASSUME_NONNULL_BEGIN
} else if ((dataMessage.flags & OWSSignalServiceProtosDataMessageFlagsExpirationTimerUpdate) != 0) {
DDLogVerbose(@"%@ Received expiration timer update message", self.tag);
[self handleExpirationTimerUpdateMessageWithEnvelope:incomingEnvelope dataMessage:dataMessage];
} else if (dataMessage.attachments.count > 0
|| (dataMessage.hasGroup && dataMessage.group.type == OWSSignalServiceProtosGroupContextTypeUpdate
&& dataMessage.group.hasAvatar)) {
DDLogVerbose(@"%@ Received push media message (attachment) or group with an avatar", self.tag);
} else if (dataMessage.attachments.count > 0) {
DDLogVerbose(@"%@ Received media message attachment", self.tag);
[self handleReceivedMediaWithEnvelope:incomingEnvelope dataMessage:dataMessage];
} else {
DDLogVerbose(@"%@ Received data message.", self.tag);
[self handleReceivedTextMessageWithEnvelope:incomingEnvelope dataMessage:dataMessage];
if ([self isDataMessageGroupAvatarUpdate:dataMessage]) {
DDLogVerbose(@"%@ Data message had group avatar attachment", self.tag);
[self handleReceivedGroupAvatarUpdateWithEnvelope:incomingEnvelope dataMessage:dataMessage];
}
}
}
- (void)handleReceivedGroupAvatarUpdateWithEnvelope:(OWSSignalServiceProtosEnvelope *)envelope
dataMessage:(OWSSignalServiceProtosDataMessage *)dataMessage
{
TSGroupThread *groupThread = [TSGroupThread getOrCreateThreadWithGroupIdData:dataMessage.group.id];
OWSAttachmentsProcessor *attachmentsProcessor =
[[OWSAttachmentsProcessor alloc] initWithAttachmentProtos:@[ dataMessage.group.avatar ]
timestamp:envelope.timestamp
relay:envelope.relay
thread:groupThread
networkManager:self.networkManager];
if (!attachmentsProcessor.hasSupportedAttachments) {
DDLogWarn(@"%@ received unsupported group avatar envelope", self.tag);
return;
}
[attachmentsProcessor fetchAttachmentsForMessage:nil
success:^(TSAttachmentStream *_Nonnull attachmentStream) {
[groupThread updateAvatarWithAttachmentStream:attachmentStream];
}
failure:^(NSError *_Nonnull error) {
DDLogError(@"%@ failed to fetch attachments for group avatar sent at: %llu. with error: %@",
self.tag,
envelope.timestamp,
error);
}];
}
- (void)handleReceivedMediaWithEnvelope:(OWSSignalServiceProtosEnvelope *)envelope
dataMessage:(OWSSignalServiceProtosDataMessage *)dataMessage
{
TSThread *thread = [self threadForEnvelope:envelope dataMessage:dataMessage];
OWSAttachmentsProcessor *attachmentsProcessor =
[[OWSAttachmentsProcessor alloc] initWithAttachmentProtos:dataMessage.attachments
timestamp:envelope.timestamp
relay:envelope.relay
thread:thread
networkManager:self.networkManager];
if (!attachmentsProcessor.hasSupportedAttachments) {
DDLogWarn(@"%@ received unsupported media envelope", self.tag);
return;
}
TSIncomingMessage *createdMessage = [self handleReceivedEnvelope:envelope
withDataMessage:dataMessage
attachmentIds:attachmentsProcessor.supportedAttachmentIds];
[attachmentsProcessor fetchAttachmentsForMessage:createdMessage
success:^(TSAttachmentStream *_Nonnull attachmentStream) {
DDLogDebug(
@"%@ successfully fetched attachment: %@ for message: %@", self.tag, attachmentStream, createdMessage);
}
failure:^(NSError *_Nonnull error) {
DDLogError(
@"%@ failed to fetch attachments for message: %@ with error: %@", self.tag, createdMessage, error);
}];
}
- (void)handleIncomingEnvelope:(OWSSignalServiceProtosEnvelope *)messageEnvelope
withSyncMessage:(OWSSignalServiceProtosSyncMessage *)syncMessage
{
@ -274,7 +349,23 @@ NS_ASSUME_NONNULL_BEGIN
DDLogInfo(@"%@ Received `sent` syncMessage, recording message transcript.", self.tag);
OWSIncomingSentMessageTranscript *transcript =
[[OWSIncomingSentMessageTranscript alloc] initWithProto:syncMessage.sent relay:messageEnvelope.relay];
[[[OWSRecordTranscriptJob alloc] initWithMessagesManager:self incomingSentMessageTranscript:transcript] run];
OWSRecordTranscriptJob *recordJob =
[[OWSRecordTranscriptJob alloc] initWithIncomingSentMessageTranscript:transcript
messageSender:self.messageSender
networkManager:self.networkManager];
if ([self isDataMessageGroupAvatarUpdate:syncMessage.sent.message]) {
[recordJob runWithAttachmentHandler:^(TSAttachmentStream *_Nonnull attachmentStream) {
TSGroupThread *groupThread =
[TSGroupThread getOrCreateThreadWithGroupIdData:syncMessage.sent.message.group.id];
[groupThread updateAvatarWithAttachmentStream:attachmentStream];
}];
} else {
[recordJob runWithAttachmentHandler:^(TSAttachmentStream *_Nonnull attachmentStream) {
DDLogDebug(@"%@ successfully fetched transcript attachment: %@", self.tag, attachmentStream);
}];
}
} else if (syncMessage.hasRequest) {
if (syncMessage.request.type == OWSSignalServiceProtosSyncMessageRequestTypeContacts) {
DDLogInfo(@"%@ Received request `Contacts` syncMessage.", self.tag);
@ -282,15 +373,14 @@ NS_ASSUME_NONNULL_BEGIN
OWSSyncContactsMessage *syncContactsMessage =
[[OWSSyncContactsMessage alloc] initWithContactsManager:self.contactsManager];
[self sendTemporaryAttachment:[syncContactsMessage buildPlainTextAttachmentData]
[self.messageSender sendTemporaryAttachmentData:[syncContactsMessage buildPlainTextAttachmentData]
contentType:OWSMimeTypeApplicationOctetStream
inMessage:syncContactsMessage
thread:nil
success:^{
DDLogInfo(@"%@ Successfully sent Contacts response syncMessage.", self.tag);
}
failure:^{
DDLogError(@"%@ Failed to send Contacts response syncMessage.", self.tag);
failure:^(NSError *error) {
DDLogError(@"%@ Failed to send Contacts response syncMessage with error: %@", self.tag, error);
}];
} else if (syncMessage.request.type == OWSSignalServiceProtosSyncMessageRequestTypeGroups) {
@ -298,15 +388,14 @@ NS_ASSUME_NONNULL_BEGIN
OWSSyncGroupsMessage *syncGroupsMessage = [[OWSSyncGroupsMessage alloc] init];
[self sendTemporaryAttachment:[syncGroupsMessage buildPlainTextAttachmentData]
[self.messageSender sendTemporaryAttachmentData:[syncGroupsMessage buildPlainTextAttachmentData]
contentType:OWSMimeTypeApplicationOctetStream
inMessage:syncGroupsMessage
thread:nil
success:^{
DDLogInfo(@"%@ Successfully sent Groups response syncMessage.", self.tag);
}
failure:^{
DDLogError(@"%@ Failed to send Groups response syncMessage.", self.tag);
failure:^(NSError *error) {
DDLogError(@"%@ Failed to send Groups response syncMessage with error: %@", self.tag, error);
}];
}
} else if (syncMessage.read.count > 0) {
@ -399,19 +488,6 @@ NS_ASSUME_NONNULL_BEGIN
[gThread saveWithTransaction:transaction];
if (dataMessage.group.type == OWSSignalServiceProtosGroupContextTypeUpdate) {
if ([attachmentIds count] == 1) {
NSString *avatarId = attachmentIds[0];
TSAttachment *avatar = [TSAttachment fetchObjectWithUniqueID:avatarId];
if ([avatar isKindOfClass:[TSAttachmentStream class]]) {
TSAttachmentStream *stream = (TSAttachmentStream *)avatar;
if ([stream isImage]) {
model.groupImage = [stream image];
// No need to keep the attachment around after assigning the image.
[stream removeWithTransaction:transaction];
}
}
}
NSString *updateGroupInfo = [gThread.groupModel getInfoStringAboutUpdateTo:model contactsManager:self.contactsManager];
gThread.groupModel = model;
[gThread saveWithTransaction:transaction];
@ -493,7 +569,8 @@ NS_ASSUME_NONNULL_BEGIN
storageManager:self.storageManager];
[readReceiptsProcessor process];
[self becomeConsistentWithDisappearingConfigurationForMessage:incomingMessage];
[self.disappearingMessagesJob becomeConsistentWithConfigurationForMessage:incomingMessage
contactsManager:self.contactsManager];
// Update thread preview in inbox
[thread touch];
@ -509,53 +586,6 @@ NS_ASSUME_NONNULL_BEGIN
return incomingMessage;
}
- (void)becomeConsistentWithDisappearingConfigurationForMessage:(TSMessage *)message
{
// Become eventually consistent in the case that the remote changed their settings at the same time.
// Also in case remote doesn't support expiring messages
OWSDisappearingMessagesConfiguration *disappearingMessagesConfiguration =
[OWSDisappearingMessagesConfiguration fetchObjectWithUniqueID:message.uniqueThreadId];
BOOL changed = NO;
if (message.expiresInSeconds == 0) {
if (disappearingMessagesConfiguration.isEnabled) {
changed = YES;
DDLogWarn(@"%@ Received remote message which had no expiration set, disabling our expiration to become "
@"consistent.",
self.tag);
disappearingMessagesConfiguration.enabled = NO;
[disappearingMessagesConfiguration save];
}
} else if (message.expiresInSeconds != disappearingMessagesConfiguration.durationSeconds) {
changed = YES;
DDLogInfo(
@"%@ Received remote message with different expiration set, updating our expiration to become consistent.",
self.tag);
disappearingMessagesConfiguration.enabled = YES;
disappearingMessagesConfiguration.durationSeconds = message.expiresInSeconds;
[disappearingMessagesConfiguration save];
}
if (!changed) {
return;
}
if ([message isKindOfClass:[TSIncomingMessage class]]) {
TSIncomingMessage *incomingMessage = (TSIncomingMessage *)message;
NSString *contactName = [self.contactsManager nameStringForPhoneIdentifier:incomingMessage.authorId];
[[[OWSDisappearingConfigurationUpdateInfoMessage alloc] initWithTimestamp:message.timestamp
thread:message.thread
configuration:disappearingMessagesConfiguration
createdByRemoteName:contactName] save];
} else {
[[[OWSDisappearingConfigurationUpdateInfoMessage alloc] initWithTimestamp:message.timestamp
thread:message.thread
configuration:disappearingMessagesConfiguration]
save];
}
}
- (void)processException:(NSException *)exception envelope:(OWSSignalServiceProtosEnvelope *)envelope
{
DDLogError(@"%@ Got exception: %@ of type: %@", self.tag, exception.description, exception.name);
@ -584,34 +614,13 @@ NS_ASSUME_NONNULL_BEGIN
}];
}
- (void)processException:(NSException *)exception
outgoingMessage:(TSOutgoingMessage *)message
inThread:(TSThread *)thread
#pragma mark - helpers
- (BOOL)isDataMessageGroupAvatarUpdate:(OWSSignalServiceProtosDataMessage *)dataMessage
{
DDLogWarn(@"%@ Got exception: %@", self.tag, exception);
[self.dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
TSErrorMessage *errorMessage;
if ([exception.name isEqualToString:UntrustedIdentityKeyException]) {
errorMessage = [TSInvalidIdentityKeySendingErrorMessage
untrustedKeyWithOutgoingMessage:message
inThread:thread
forRecipient:exception.userInfo[TSInvalidRecipientKey]
preKeyBundle:exception.userInfo[TSInvalidPreKeyBundleKey]
withTransaction:transaction];
message.messageState = TSOutgoingMessageStateUnsent;
[message saveWithTransaction:transaction];
} else if (message.groupMetaMessage == TSGroupMessageNone) {
// Only update this with exception if it is not a group message as group
// messages may except for one group
// send but not another and the UI doesn't know how to handle that
[message setMessageState:TSOutgoingMessageStateUnsent];
[message saveWithTransaction:transaction];
}
[errorMessage saveWithTransaction:transaction];
}];
return dataMessage.hasGroup
&& dataMessage.group.type == OWSSignalServiceProtosGroupContextTypeUpdate
&& dataMessage.group.hasAvatar;
}
- (TSThread *)threadForEnvelope:(OWSSignalServiceProtosEnvelope *)envelope

View File

@ -24,8 +24,9 @@ NS_ASSUME_NONNULL_BEGIN
if (devices) {
successCallback(devices);
} else {
failureCallback(OWSErrorWithCodeDescription(
OWSErrorCodeUnableToProcessServerResponse, @"Unable to parse server response"));
DDLogError(@"%@ unable to parse devices response:%@", self.tag, responseObject);
NSError *error = OWSErrorMakeUnableToProcessServerResponseError();
failureCallback(error);
}
}
failure:^(NSURLSessionDataTask *task, NSError *error) {
@ -79,6 +80,18 @@ NS_ASSUME_NONNULL_BEGIN
return [devices copy];
}
#pragma mark - Logging
+ (NSString *)tag
{
return [NSString stringWithFormat:@"[%@]", self.class];
}
- (NSString *)tag
{
return self.class.tag;
}
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,22 @@
// Created by Michael Kirk on 10/18/16.
// Copyright © 2016 Open Whisper Systems. All rights reserved.
NS_ASSUME_NONNULL_BEGIN
@class TSOutgoingMessage;
@class TSNetworkManager;
@class TSAttachmentStream;
@interface OWSUploadingService : NSObject
- (instancetype)init NS_UNAVAILABLE;
- (instancetype)initWithNetworkManager:(TSNetworkManager *)networkManager NS_DESIGNATED_INITIALIZER;
- (void)uploadAttachmentStream:(TSAttachmentStream *)attachmentStream
message:(TSOutgoingMessage *)outgoingMessage
success:(void (^)())successHandler
failure:(void (^)(NSError *error))failureHandler;
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,157 @@
// Created by Michael Kirk on 10/18/16.
// Copyright © 2016 Open Whisper Systems. All rights reserved.
#import "OWSUploadingService.h"
#import "Cryptography.h"
#import "MIMETypeUtil.h"
#import "OWSDispatch.h"
#import "OWSError.h"
#import "TSAttachmentStream.h"
#import "TSNetworkManager.h"
#import "TSOutgoingMessage.h"
NS_ASSUME_NONNULL_BEGIN
@interface OWSUploadingService ()
@property (nonatomic, readonly) TSNetworkManager *networkManager;
@end
@implementation OWSUploadingService
- (instancetype)initWithNetworkManager:(TSNetworkManager *)networkManager
{
self = [super init];
if (!self) {
return self;
}
_networkManager = networkManager;
return self;
}
- (void)uploadAttachmentStream:(TSAttachmentStream *)attachmentStream
message:(TSOutgoingMessage *)outgoingMessage
success:(void (^)())successHandler
failure:(void (^)(NSError *_Nonnull))failureHandler
{
outgoingMessage.messageState = TSOutgoingMessageStateAttemptingOut;
[outgoingMessage save];
if (attachmentStream.serverId) {
DDLogDebug(@"%@ Attachment previously uploaded.", self.tag);
successHandler(outgoingMessage);
return;
}
TSRequest *allocateAttachment = [[TSAllocAttachmentRequest alloc] init];
[self.networkManager makeRequest:allocateAttachment
success:^(NSURLSessionDataTask *task, id responseObject) {
dispatch_async([OWSDispatch attachmentsQueue], ^{ // TODO can we move this queue specification up a level?
if (![responseObject isKindOfClass:[NSDictionary class]]) {
DDLogError(@"%@ unexpected response from server: %@", self.tag, responseObject);
NSError *error = OWSErrorMakeUnableToProcessServerResponseError();
return failureHandler(error);
}
NSDictionary *responseDict = (NSDictionary *)responseObject;
UInt64 serverId = ((NSDecimalNumber *)[responseDict objectForKey:@"id"]).unsignedLongLongValue;
NSString *location = [responseDict objectForKey:@"location"];
NSError *error;
NSData *attachmentData = [attachmentStream readDataFromFileWithError:&error];
if (error) {
DDLogError(@"%@ Failed to read attachment data with error:%@", self.tag, error);
return failureHandler(error);
}
NSData *encryptionKey;
NSData *encryptedAttachmentData =
[Cryptography encryptAttachmentData:attachmentData outKey:&encryptionKey];
attachmentStream.encryptionKey = encryptionKey;
[self uploadDataWithProgress:encryptedAttachmentData
location:location
attachmentId:attachmentStream.uniqueId
success:^{
DDLogInfo(@"%@ Uploaded attachment.", self.tag);
attachmentStream.serverId = serverId;
attachmentStream.isDownloaded = YES;
[attachmentStream save];
successHandler();
}
failure:failureHandler];
});
}
failure:^(NSURLSessionDataTask *task, NSError *error) {
DDLogError(@"%@ Failed to allocate attachment with error: %@", self.tag, error);
failureHandler(error);
}];
}
- (void)uploadDataWithProgress:(NSData *)cipherText
location:(NSString *)location
attachmentId:(NSString *)attachmentId
success:(void (^)())successHandler
failure:(void (^)(NSError *error))failureHandler
{
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:[NSURL URLWithString:location]];
request.HTTPMethod = @"PUT";
request.HTTPBody = cipherText;
[request setValue:OWSMimeTypeApplicationOctetStream forHTTPHeaderField:@"Content-Type"];
AFURLSessionManager *manager = [[AFURLSessionManager alloc]
initWithSessionConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]];
NSURLSessionUploadTask *uploadTask;
uploadTask = [manager uploadTaskWithRequest:request
fromData:cipherText
progress:^(NSProgress *_Nonnull uploadProgress) {
NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];
[notificationCenter postNotificationName:@"attachmentUploadProgress"
object:nil
userInfo:@{
@"progress" : @(uploadProgress.fractionCompleted),
@"attachmentId" : attachmentId
}];
}
completionHandler:^(NSURLResponse *_Nonnull response, id _Nullable responseObject, NSError *_Nullable error) {
if (error) {
return failureHandler(error);
}
NSInteger statusCode = ((NSHTTPURLResponse *)response).statusCode;
BOOL isValidResponse = (statusCode >= 200) && (statusCode < 400);
if (!isValidResponse) {
DDLogError(@"%@ Unexpected server response: %d", self.tag, (int)statusCode);
NSError *invalidResponseError = OWSErrorMakeUnableToProcessServerResponseError();
return failureHandler(invalidResponseError);
}
successHandler();
}];
[uploadTask resume];
}
#pragma mark - Logging
+ (NSString *)tag
{
return [NSString stringWithFormat:@"[%@]", self.class];
}
- (NSString *)tag
{
return self.class.tag;
}
@end
NS_ASSUME_NONNULL_END

View File

@ -10,6 +10,6 @@
@interface TSAttachmentRequest : TSRequest
- (TSRequest *)initWithId:(NSNumber *)attachmentId relay:(NSString *)relay;
- (TSRequest *)initWithId:(UInt64)attachmentId relay:(NSString *)relay;
@end

View File

@ -11,8 +11,9 @@
@implementation TSAttachmentRequest
- (TSRequest *)initWithId:(NSNumber *)attachmentId relay:(NSString *)relay {
NSString *path = [NSString stringWithFormat:@"%@/%@", textSecureAttachmentsAPI, attachmentId];
- (TSRequest *)initWithId:(UInt64)attachmentId relay:(NSString *)relay
{
NSString *path = [NSString stringWithFormat:@"%@/%llu", textSecureAttachmentsAPI, attachmentId];
if (relay && ![relay isEqualToString:@""]) {
path = [path stringByAppendingFormat:@"?relay=%@", relay];

View File

@ -34,19 +34,31 @@ typedef void (^failureBlock)(NSURLSessionDataTask *task, NSError *error);
static TSNetworkManager *sharedMyManager = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedMyManager = [[self alloc] init];
sharedMyManager = [[self alloc] initWithDefaultOperationManager];
});
return sharedMyManager;
}
- (id)init {
if (self = [super init]) {
NSURLSessionConfiguration *sessionConf = NSURLSessionConfiguration.ephemeralSessionConfiguration;
self.operationManager =
[[AFHTTPSessionManager alloc] initWithBaseURL:[[NSURL alloc] initWithString:textSecureServerURL]
sessionConfiguration:sessionConf];
self.operationManager.securityPolicy = [OWSHTTPSecurityPolicy sharedPolicy];
- (instancetype)initWithDefaultOperationManager
{
NSURLSessionConfiguration *sessionConf = NSURLSessionConfiguration.ephemeralSessionConfiguration;
NSURL *baseURL = [[NSURL alloc] initWithString:textSecureServerURL];
AFHTTPSessionManager *operationManager =
[[AFHTTPSessionManager alloc] initWithBaseURL:baseURL sessionConfiguration:sessionConf];
operationManager.securityPolicy = [OWSHTTPSecurityPolicy sharedPolicy];
return [self initWithOperationManager:operationManager];
}
- (instancetype)initWithOperationManager:(AFHTTPSessionManager *)operationManager
{
self = [super init];
if (!self) {
return self;
}
_operationManager = operationManager;
return self;
}

View File

@ -84,7 +84,11 @@
*/
- (void)saveWithTransaction:(YapDatabaseReadWriteTransaction *)transaction;
/**
* `touch` is a cheap way to fire a YapDatabaseModified notification to redraw anythign depending on the model.
*/
- (void)touch;
- (void)touchWithTransaction:(YapDatabaseReadWriteTransaction *)transaction;
/**
* The unique identifier of the stored object

View File

@ -36,10 +36,15 @@
}];
}
- (void)touchWithTransaction:(YapDatabaseReadWriteTransaction *)transaction
{
[transaction touchObjectForKey:self.uniqueId inCollection:[self.class collection]];
}
- (void)touch
{
[[self dbConnection] readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
[transaction touchObjectForKey:self.uniqueId inCollection:[self.class collection]];
[self touchWithTransaction:transaction];
}];
}

View File

@ -1,13 +1,5 @@
//
// Cryptography.h
// TextSecureiOS
//
// Created by Christine Corbett Moran on 3/26/13.
// Copyright (c) 2013 Open Whisper Systems. All rights reserved.
//
#import <Foundation/Foundation.h>
#import "TSAttachmentEncryptionResult.h"
@interface Cryptography : NSObject
@ -34,7 +26,6 @@ typedef NS_ENUM(NSInteger, TSMACType) {
#pragma mark encrypt and decrypt attachment data
+ (NSData *)decryptAttachment:(NSData *)dataToDecrypt withKey:(NSData *)key;
+ (TSAttachmentEncryptionResult *)encryptAttachment:(NSData *)attachment
contentType:(NSString *)contentType
identifier:(NSString *)identifier;
+ (NSData *)encryptAttachmentData:(NSData *)attachmentData outKey:(NSData **)outKey;
@end

View File

@ -278,20 +278,20 @@
matchingHMAC:hmac];
}
+ (TSAttachmentEncryptionResult *)encryptAttachment:(NSData *)attachment
contentType:(NSString *)contentType
identifier:(NSString *)identifier {
+ (NSData *)encryptAttachmentData:(NSData *)attachmentData outKey:(NSData **)outKey
{
NSData *iv = [Cryptography generateRandomBytes:AES_CBC_IV_LENGTH];
NSData *encryptionKey = [Cryptography generateRandomBytes:AES_KEY_SIZE];
NSData *hmacKey = [Cryptography generateRandomBytes:HMAC256_KEY_LENGTH];
// The concatenated key for storage
NSMutableData *outKey = [NSMutableData data];
[outKey appendData:encryptionKey];
[outKey appendData:hmacKey];
NSMutableData *key = [NSMutableData data];
[key appendData:encryptionKey];
[key appendData:hmacKey];
*outKey = [key copy];
NSData *computedHMAC;
NSData *ciphertext = [Cryptography encryptCBCMode:attachment
NSData *ciphertext = [Cryptography encryptCBCMode:attachmentData
withKey:encryptionKey
withIV:iv
withVersion:nil
@ -299,15 +299,12 @@
withHMACType:TSHMACSHA256AttachementType
computedHMAC:&computedHMAC];
NSMutableData *encryptedAttachment = [NSMutableData data];
[encryptedAttachment appendData:iv];
[encryptedAttachment appendData:ciphertext];
[encryptedAttachment appendData:computedHMAC];
NSMutableData *encryptedAttachmentData = [NSMutableData data];
[encryptedAttachmentData appendData:iv];
[encryptedAttachmentData appendData:ciphertext];
[encryptedAttachmentData appendData:computedHMAC];
TSAttachmentStream *pointer =
[[TSAttachmentStream alloc] initWithIdentifier:identifier data:attachment key:outKey contentType:contentType];
return [[TSAttachmentEncryptionResult alloc] initWithPointer:pointer body:encryptedAttachment];
return encryptedAttachmentData;
}
@end

12
src/Util/OWSDispatch.h Normal file
View File

@ -0,0 +1,12 @@
NS_ASSUME_NONNULL_BEGIN
@interface OWSDispatch : NSObject
+ (dispatch_queue_t)attachmentsQueue;
+ (dispatch_queue_t)sendingQueue;
@end
NS_ASSUME_NONNULL_END

32
src/Util/OWSDispatch.m Normal file
View File

@ -0,0 +1,32 @@
// Created by Michael Kirk on 10/18/16.
// Copyright © 2016 Open Whisper Systems. All rights reserved.
#import "OWSDispatch.h"
NS_ASSUME_NONNULL_BEGIN
@implementation OWSDispatch
+ (dispatch_queue_t)attachmentsQueue
{
static dispatch_once_t onceToken;
static dispatch_queue_t queue;
dispatch_once(&onceToken, ^{
queue = dispatch_queue_create("org.whispersystems.signal.attachments", NULL);
});
return queue;
}
+ (dispatch_queue_t)sendingQueue
{
static dispatch_once_t onceToken;
static dispatch_queue_t queue;
dispatch_once(&onceToken, ^{
queue = dispatch_queue_create("org.whispersystems.signal.sendQueue", NULL);
});
return queue;
}
@end
NS_ASSUME_NONNULL_END

View File

@ -11,9 +11,12 @@ typedef NS_ENUM(NSInteger, OWSErrorCode) {
OWSErrorCodeFailedToEncodeJson = 14,
OWSErrorCodeFailedToDecodeQR = 15,
OWSErrorCodePrivacyVerificationFailure = 20,
OWSErrorCodeFailedToSendOutgoingMessage = 30
OWSErrorCodeFailedToSendOutgoingMessage = 30,
OWSErrorCodeFailedToDecryptMessage = 100
};
extern NSError *OWSErrorWithCodeDescription(OWSErrorCode code, NSString *description);
extern NSError *OWSErrorMakeUnableToProcessServerResponseError();
extern NSError *OWSErrorMakeFailedToSendOutgoingMessageError();
NS_ASSUME_NONNULL_END

View File

@ -13,4 +13,16 @@ NSError *OWSErrorWithCodeDescription(OWSErrorCode code, NSString *description)
userInfo:@{ NSLocalizedDescriptionKey: description }];
}
NSError *OWSErrorMakeUnableToProcessServerResponseError()
{
return OWSErrorWithCodeDescription(OWSErrorCodeUnableToProcessServerResponse,
NSLocalizedString(@"ERROR_DESCRIPTION_SERVER_FAILURE", @"Generic server error"));
}
NSError *OWSErrorMakeFailedToSendOutgoingMessageError()
{
return OWSErrorWithCodeDescription(OWSErrorCodeFailedToSendOutgoingMessage,
NSLocalizedString(@"ERROR_DESCRIPTION_CLIENT_SENDING_FAILURE", @"Generic notice when message failed to send."));
}
NS_ASSUME_NONNULL_END

View File

@ -1,19 +0,0 @@
//
// TSAttachmentEncryptionResult.h
// Signal
//
// Created by Frederic Jacobs on 21/12/14.
// Copyright (c) 2014 Open Whisper Systems. All rights reserved.
//
#import <Foundation/Foundation.h>
#import "TSAttachmentStream.h"
@interface TSAttachmentEncryptionResult : NSData
@property (readwrite) TSAttachmentStream *pointer;
@property (readonly) NSData *body;
- (instancetype)initWithPointer:(TSAttachmentStream *)pointer body:(NSData *)cipherText;
@end

View File

@ -1,24 +0,0 @@
//
// TSAttachmentEncryptionResult.m
// Signal
//
// Created by Frederic Jacobs on 21/12/14.
// Copyright (c) 2014 Open Whisper Systems. All rights reserved.
//
#import "TSAttachmentEncryptionResult.h"
@implementation TSAttachmentEncryptionResult
- (instancetype)initWithPointer:(TSAttachmentStream *)pointer body:(NSData *)cipherText {
self = [super init];
if (self) {
_body = cipherText;
_pointer = pointer;
}
return self;
}
@end

View File

@ -1,43 +1,30 @@
//
// TSAttachementsTest.m
// Signal
//
// Created by Frederic Jacobs on 21/12/14.
// Copyright (c) 2014 Open Whisper Systems. All rights reserved.
//
#import <XCTest/XCTest.h>
#import "TSAttachmentStream.h"
#import "Cryptography.h"
NS_ASSUME_NONNULL_BEGIN
@interface TSAttachmentsTest : XCTestCase
@end
@implementation TSAttachmentsTest
- (void)setUp {
[super setUp];
// Put setup code here. This method is called before the invocation of each test method in the class.
}
- (void)tearDown {
// Put teardown code here. This method is called after the invocation of each test method in the class.
[super tearDown];
}
- (void)testAttachmentEncryptionDecryption {
- (void)testAttachmentEncryptionDecryption
{
NSData *plaintext = [Cryptography generateRandomBytes:100];
NSString *contentType = @"img/jpg";
uint64_t identifier = 3063578577793591963;
NSNumber *number = [NSNumber numberWithUnsignedLongLong:identifier];
TSAttachmentEncryptionResult *encryptionResult = [Cryptography encryptAttachment:plaintext contentType:contentType identifier:[number stringValue]];
NSData *plaintextBis = [Cryptography decryptAttachment:encryptionResult.body withKey:encryptionResult.pointer.encryptionKey];
NSData *encryptionKey;
NSData *encryptedData = [Cryptography encryptAttachmentData:plaintext outKey:&encryptionKey];
NSData *plaintextBis = [Cryptography decryptAttachment:encryptedData withKey:encryptionKey];
XCTAssert([plaintext isEqualToData:plaintextBis], @"Attachments encryption failed");
}
@end
NS_ASSUME_NONNULL_END

View File

@ -61,11 +61,9 @@
[TSInteraction removeAllObjectsInCollection];
XCTAssertEqual(0, [thread numberOfInteractions]);
TSAttachmentStream *incomingAttachment =
[[TSAttachmentStream alloc] initWithIdentifier:@"fake-incoming-photo-attachment-id"
data:[[NSData alloc] init]
key:[[NSData alloc] init]
contentType:@"image/jpeg"];
NSError *error;
TSAttachmentStream *incomingAttachment = [[TSAttachmentStream alloc] initWithContentType:@"image/jpeg"];
[incomingAttachment writeData:[NSData new] error:&error];
[incomingAttachment save];
// Sanity check
@ -80,11 +78,8 @@
attachmentIds:[NSMutableArray arrayWithObject:incomingAttachment.uniqueId]];
[incomingMessage save];
TSAttachmentStream *outgoingAttachment =
[[TSAttachmentStream alloc] initWithIdentifier:@"fake-outgoing-photo-attachment-id"
data:[[NSData alloc] init]
key:[[NSData alloc] init]
contentType:@"image/jpeg"];
TSAttachmentStream *outgoingAttachment = [[TSAttachmentStream alloc] initWithContentType:@"image/jpeg"];
[outgoingAttachment writeData:[NSData new] error:&error];
[outgoingAttachment save];
// Sanity check

View File

@ -3,6 +3,7 @@
#import "OWSDeviceProvisioner.h"
#import "OWSDeviceProvisioningCodeService.h"
#import "OWSDeviceProvisioningService.h"
#import "OWSFakeNetworkManager.h"
#import "TSNetworkManager.h"
#import <XCTest/XCTest.h>
@ -46,22 +47,6 @@
@end
@interface OWSFakeNetworkManager : TSNetworkManager
- (instancetype)init NS_UNAVAILABLE;
- (instancetype)initFake;
@end
@implementation OWSFakeNetworkManager
- (instancetype)initFake
{
return self;
}
@end
@interface OWSDeviceProvisionerTest : XCTestCase
@end
@ -80,7 +65,7 @@
NSString *accountIdentifier;
NSString *theirEphemeralDeviceId;
OWSFakeNetworkManager *networkManager = [[OWSFakeNetworkManager alloc] initFake];
OWSFakeNetworkManager *networkManager = [OWSFakeNetworkManager new];
OWSDeviceProvisioner *provisioner = [[OWSDeviceProvisioner alloc]
initWithMyPublicKey:myPublicKey
myPrivateKey:myPrivateKey

View File

@ -1,7 +1,7 @@
// Copyright © 2016 Open Whisper Systems. All rights reserved.
#import "NSDate+millisecondTimeStamp.h"
#import "TSAttachment.h"
#import "TSAttachmentStream.h"
#import "TSMessage.h"
#import "TSThread.h"
@ -75,15 +75,13 @@ NS_ASSUME_NONNULL_BEGIN
- (void)testDescriptionWithPhotoAttachmentId
{
TSAttachment *attachment = [[TSAttachment alloc] initWithIdentifier:@"fake-photo-attachment-id"
encryptionKey:[[NSData alloc] init]
contentType:@"image/jpeg"];
TSAttachment *attachment = [[TSAttachmentStream alloc] initWithContentType:@"image/jpeg"];
[attachment save];
TSMessage *message = [[TSMessage alloc] initWithTimestamp:1
inThread:self.thread
messageBody:@"My message body"
attachmentIds:@[ @"fake-photo-attachment-id" ]];
attachmentIds:@[ attachment.uniqueId ]];
NSString *actualDescription = [message description];
XCTAssertEqualObjects(@"📷 ATTACHMENT", actualDescription);
}
@ -91,15 +89,13 @@ NS_ASSUME_NONNULL_BEGIN
- (void)testDescriptionWithVideoAttachmentId
{
TSAttachment *attachment = [[TSAttachment alloc] initWithIdentifier:@"fake-video-attachment-id"
encryptionKey:[[NSData alloc] init]
contentType:@"video/mp4"];
TSAttachment *attachment = [[TSAttachmentStream alloc] initWithContentType:@"video/mp4"];
[attachment save];
TSMessage *message = [[TSMessage alloc] initWithTimestamp:1
inThread:self.thread
messageBody:@"My message body"
attachmentIds:@[ @"fake-video-attachment-id" ]];
attachmentIds:@[ attachment.uniqueId ]];
NSString *actualDescription = [message description];
XCTAssertEqualObjects(@"📽 ATTACHMENT", actualDescription);
}
@ -107,30 +103,26 @@ NS_ASSUME_NONNULL_BEGIN
- (void)testDescriptionWithAudioAttachmentId
{
TSAttachment *attachment = [[TSAttachment alloc] initWithIdentifier:@"fake-audio-attachment-id"
encryptionKey:[[NSData alloc] init]
contentType:@"audio/mp3"];
TSAttachment *attachment = [[TSAttachmentStream alloc] initWithContentType:@"audio/mp3"];
[attachment save];
TSMessage *message = [[TSMessage alloc] initWithTimestamp:1
inThread:self.thread
messageBody:@"My message body"
attachmentIds:@[ @"fake-audio-attachment-id" ]];
attachmentIds:@[ attachment.uniqueId ]];
NSString *actualDescription = [message description];
XCTAssertEqualObjects(@"📻 ATTACHMENT", actualDescription);
}
- (void)testDescriptionWithUnkownAudioContentType
{
TSAttachment *attachment = [[TSAttachment alloc] initWithIdentifier:@"fake-nonsense-attachment-id"
encryptionKey:[[NSData alloc] init]
contentType:@"non/sense"];
TSAttachment *attachment = [[TSAttachmentStream alloc] initWithContentType:@"non/sense"];
[attachment save];
TSMessage *message = [[TSMessage alloc] initWithTimestamp:1
inThread:self.thread
messageBody:@"My message body"
attachmentIds:@[ @"fake-nonsense-attachment-id" ]];
attachmentIds:@[ attachment.uniqueId ]];
NSString *actualDescription = [message description];
XCTAssertEqualObjects(@"ATTACHMENT", actualDescription);
}

View File

@ -4,8 +4,11 @@
#import "OWSError.h"
#import "OWSFakeContactsManager.h"
#import "OWSFakeContactsUpdater.h"
#import "OWSFakeNetworkManager.h"
#import "OWSMessageSender.h"
#import "OWSUploadingService.h"
#import "TSContactThread.h"
#import "TSMessagesManager.h"
#import "TSNetworkManager.h"
#import "TSOutgoingMessage.h"
#import "TSStorageManager+keyingMaterial.h"
@ -14,6 +17,82 @@
NS_ASSUME_NONNULL_BEGIN
@interface OWSMessageSender (Testing)
@property (nonatomic) OWSUploadingService *uploadingService;
@property (nonatomic) ContactsUpdater *contactsUpdater;
// Private Methods to test
- (NSArray<SignalRecipient *> *)getRecipients:(NSArray<NSString *> *)identifiers error:(NSError **)error;
@end
@implementation OWSMessageSender (Testing)
- (NSArray<NSDictionary *> *)deviceMessages:(TSOutgoingMessage *)message
forRecipient:(SignalRecipient *)recipient
inThread:(TSThread *)thread
{
NSLog(@"[OWSFakeMessagesManager] Faking deviceMessages.");
return @[];
}
- (void)setContactsUpdater:(ContactsUpdater *)contactsUpdater
{
_contactsUpdater = contactsUpdater;
}
- (ContactsUpdater *)contactsUpdater
{
return _contactsUpdater;
}
- (void)setUploadingService:(OWSUploadingService *)uploadingService
{
_uploadingService = uploadingService;
}
- (OWSUploadingService *)uploadingService
{
return _uploadingService;
}
@end
@interface OWSFakeUploadingService : OWSUploadingService
@property (nonatomic, readonly) BOOL shouldSucceed;
@end
@implementation OWSFakeUploadingService
- (instancetype)initWithSuccess:(BOOL)flag
{
self = [super initWithNetworkManager:[OWSFakeNetworkManager new]];
if (!self) {
return self;
}
_shouldSucceed = flag;
return self;
}
- (void)uploadAttachmentStream:(TSAttachmentStream *)attachmentStream
message:(TSOutgoingMessage *)outgoingMessage
success:(void (^)())successHandler
failure:(void (^)(NSError *error))failureHandler
{
if (self.shouldSucceed) {
successHandler();
} else {
failureHandler(OWSErrorMakeFailedToSendOutgoingMessageError());
}
}
@end
@interface OWSFakeURLSessionDataTask : NSURLSessionDataTask
@property (copy) NSHTTPURLResponse *response;
@ -42,7 +121,7 @@ NS_ASSUME_NONNULL_BEGIN
@end
@interface OWSMessageSenderFakeNetworkManager : TSNetworkManager
@interface OWSMessageSenderFakeNetworkManager : OWSFakeNetworkManager
- (instancetype)init NS_UNAVAILABLE;
- (instancetype)initWithSuccess:(BOOL)shouldSucceed NS_DESIGNATED_INITIALIZER;
@ -55,7 +134,11 @@ NS_ASSUME_NONNULL_BEGIN
- (instancetype)initWithSuccess:(BOOL)shouldSucceed
{
// intentionally skipping super init which explodes without setup.
self = [super init];
if (!self) {
return self;
}
_shouldSucceed = shouldSucceed;
return self;
@ -75,7 +158,7 @@ NS_ASSUME_NONNULL_BEGIN
failure(task, error);
}
} else {
NSLog(@"Ignoring unhandled request: %@", request);
[super makeRequest:request success:success failure:failure];
}
}
@ -86,6 +169,8 @@ NS_ASSUME_NONNULL_BEGIN
@property (nonatomic) TSThread *thread;
@property (nonatomic) TSOutgoingMessage *expiringMessage;
@property (nonatomic) OWSMessageSenderFakeNetworkManager *networkManager;
@property (nonatomic) OWSMessageSender *successfulMessageSender;
@property (nonatomic) OWSMessageSender *unsuccessfulMessageSender;
@end
@ -107,28 +192,42 @@ NS_ASSUME_NONNULL_BEGIN
attachmentIds:[NSMutableArray new]
expiresInSeconds:30];
[self.expiringMessage save];
TSStorageManager *storageManager = [TSStorageManager sharedManager];
OWSFakeContactsManager *contactsManager = [OWSFakeContactsManager new];
OWSFakeContactsUpdater *contactsUpdater = [OWSFakeContactsUpdater new];
// Successful Sending
TSNetworkManager *successfulNetworkManager = [[OWSMessageSenderFakeNetworkManager alloc] initWithSuccess:YES];
self.successfulMessageSender = [[OWSMessageSender alloc] initWithNetworkManager:successfulNetworkManager
storageManager:storageManager
contactsManager:contactsManager
contactsUpdater:contactsUpdater];
// Unsuccessful Sending
TSNetworkManager *unsuccessfulNetworkManager = [[OWSMessageSenderFakeNetworkManager alloc] initWithSuccess:NO];
self.unsuccessfulMessageSender = [[OWSMessageSender alloc] initWithNetworkManager:unsuccessfulNetworkManager
storageManager:storageManager
contactsManager:contactsManager
contactsUpdater:contactsUpdater];
}
- (void)testExpiringMessageTimerStartsOnSuccess
{
TSNetworkManager *networkManager = [[OWSMessageSenderFakeNetworkManager alloc] initWithSuccess:YES];
OWSMessageSender *messageSender = [[OWSMessageSender alloc] initWithMessage:self.expiringMessage
networkManager:networkManager
storageManager:[TSStorageManager sharedManager]
contactsManager:[OWSFakeContactsManager new]
contactsUpdater:[OWSFakeContactsUpdater new]];
OWSMessageSender *messageSender = self.successfulMessageSender;
// Sanity Check
XCTAssertEqual(0, self.expiringMessage.expiresAt);
XCTestExpectation *messageStartedExpiration = [self expectationWithDescription:@"messageStartedExpiration"];
[messageSender sendWithSuccess:^() {
if (self.expiringMessage.expiresAt > 0) {
[messageStartedExpiration fulfill];
} else {
XCTFail(@"Message expiration was supposed to start.");
[messageSender sendMessage:self.expiringMessage
success:^() {
if (self.expiringMessage.expiresAt > 0) {
[messageStartedExpiration fulfill];
} else {
XCTFail(@"Message expiration was supposed to start.");
}
}
}
failure:^(NSError *error) {
XCTFail(@"Message failed to send");
}];
@ -141,20 +240,16 @@ NS_ASSUME_NONNULL_BEGIN
- (void)testExpiringMessageTimerDoesNotStartsOnFailure
{
TSNetworkManager *networkManager = [[OWSMessageSenderFakeNetworkManager alloc] initWithSuccess:NO];
OWSMessageSender *messageSender = [[OWSMessageSender alloc] initWithMessage:self.expiringMessage
networkManager:networkManager
storageManager:[TSStorageManager sharedManager]
contactsManager:[OWSFakeContactsManager new]
contactsUpdater:[OWSFakeContactsUpdater new]];
OWSMessageSender *messageSender = self.unsuccessfulMessageSender;
// Sanity Check
XCTAssertEqual(0, self.expiringMessage.expiresAt);
XCTestExpectation *messageDidNotStartExpiration = [self expectationWithDescription:@"messageStartedExpiration"];
[messageSender sendWithSuccess:^() {
XCTFail(@"Message sending was supposed to fail.");
}
[messageSender sendMessage:self.expiringMessage
success:^() {
XCTFail(@"Message sending was supposed to fail.");
}
failure:^(NSError *error) {
if (self.expiringMessage.expiresAt == 0) {
[messageDidNotStartExpiration fulfill];
@ -163,10 +258,157 @@ NS_ASSUME_NONNULL_BEGIN
}
}];
[self waitForExpectationsWithTimeout:5
handler:^(NSError *error) {
NSLog(@"Wasn't able to verify.");
}];
[self waitForExpectationsWithTimeout:5 handler:nil];
}
- (void)testTextMessageIsMarkedAsSentOnSuccess
{
OWSMessageSender *messageSender = self.successfulMessageSender;
TSOutgoingMessage *message = [[TSOutgoingMessage alloc] initWithTimestamp:1
inThread:self.thread
messageBody:@"We want punks in the palace."];
XCTestExpectation *markedAsSent = [self expectationWithDescription:@"markedAsSent"];
[messageSender sendMessage:message
success:^() {
if (message.messageState == TSOutgoingMessageStateSent) {
[markedAsSent fulfill];
} else {
XCTFail(@"Unexpected message state");
}
}
failure:^(NSError *error) {
XCTFail(@"sendMessage should succeed.");
}];
[self waitForExpectationsWithTimeout:5 handler:nil];
}
- (void)testMediaMessageIsMarkedAsSentOnSuccess
{
OWSMessageSender *messageSender = self.successfulMessageSender;
messageSender.uploadingService = [[OWSFakeUploadingService alloc] initWithSuccess:YES];
TSOutgoingMessage *message = [[TSOutgoingMessage alloc] initWithTimestamp:1
inThread:self.thread
messageBody:@"We want punks in the palace."];
XCTestExpectation *markedAsSent = [self expectationWithDescription:@"markedAsSent"];
[messageSender sendAttachmentData:[NSData new]
contentType:@"image/gif"
inMessage:message
success:^() {
if (message.messageState == TSOutgoingMessageStateSent) {
[markedAsSent fulfill];
} else {
XCTFail(@"Unexpected message state");
}
}
failure:^(NSError *error) {
XCTFail(@"sendMessage should succeed.");
}];
[self waitForExpectationsWithTimeout:5 handler:nil];
}
- (void)testTextMessageIsMarkedAsUnsentOnFailure
{
OWSMessageSender *messageSender = self.unsuccessfulMessageSender;
messageSender.uploadingService = [[OWSFakeUploadingService alloc] initWithSuccess:YES];
TSOutgoingMessage *message = [[TSOutgoingMessage alloc] initWithTimestamp:1
inThread:self.thread
messageBody:@"We want punks in the palace."];
XCTestExpectation *markedAsUnsent = [self expectationWithDescription:@"markedAsUnsent"];
[messageSender sendMessage:message
success:^() {
XCTFail(@"sendMessage should fail.");
}
failure:^(NSError *error) {
if (message.messageState == TSOutgoingMessageStateUnsent) {
[markedAsUnsent fulfill];
} else {
XCTFail(@"Unexpected message state");
}
}];
[self waitForExpectationsWithTimeout:5 handler:nil];
}
- (void)testMediaMessageIsMarkedAsUnsentOnFailureToSend
{
OWSMessageSender *messageSender = self.unsuccessfulMessageSender;
// Assume that upload will go well, but that failure happens elsewhere in message sender.
messageSender.uploadingService = [[OWSFakeUploadingService alloc] initWithSuccess:YES];
TSOutgoingMessage *message = [[TSOutgoingMessage alloc] initWithTimestamp:1
inThread:self.thread
messageBody:@"We want punks in the palace."];
XCTestExpectation *markedAsUnsent = [self expectationWithDescription:@"markedAsUnsent"];
[messageSender sendAttachmentData:[NSData new]
contentType:@"image/gif"
inMessage:message
success:^{
XCTFail(@"sendMessage should fail.");
}
failure:^(NSError *_Nonnull error) {
if (message.messageState == TSOutgoingMessageStateUnsent) {
[markedAsUnsent fulfill];
} else {
XCTFail(@"Unexpected message state");
}
}];
[self waitForExpectationsWithTimeout:5 handler:nil];
}
- (void)testMediaMessageIsMarkedAsUnsentOnFailureToUpload
{
OWSMessageSender *messageSender = self.successfulMessageSender;
// Assume that upload fails, but other sending stuff would succeed.
messageSender.uploadingService = [[OWSFakeUploadingService alloc] initWithSuccess:NO];
TSOutgoingMessage *message = [[TSOutgoingMessage alloc] initWithTimestamp:1
inThread:self.thread
messageBody:@"We want punks in the palace."];
XCTestExpectation *markedAsUnsent = [self expectationWithDescription:@"markedAsUnsent"];
[messageSender sendAttachmentData:[NSData new]
contentType:@"image/gif"
inMessage:message
success:^{
XCTFail(@"sendMessage should fail.");
}
failure:^(NSError *_Nonnull error) {
if (message.messageState == TSOutgoingMessageStateUnsent) {
[markedAsUnsent fulfill];
} else {
XCTFail(@"Unexpected message state");
}
}];
[self waitForExpectationsWithTimeout:5 handler:nil];
}
- (void)testGetRecipients
{
SignalRecipient *recipient =
[[SignalRecipient alloc] initWithTextSecureIdentifier:@"fake-recipient-id" relay:nil supportsVoice:YES];
[recipient save];
OWSMessageSender *messageSender = self.successfulMessageSender;
// At the time of writing this test, the ContactsUpdater was relying on global singletons. So if this test
// later fails due to network access that could be why.
messageSender.contactsUpdater = [ContactsUpdater sharedUpdater];
NSError *error;
NSArray<SignalRecipient *> *recipients = [messageSender getRecipients:@[ recipient.uniqueId ] error:&error];
XCTAssertNil(error);
XCTAssertEqualObjects(recipient, recipients.firstObject);
}
@end

View File

@ -5,17 +5,18 @@
#import "ContactsManagerProtocol.h"
#import "ContactsUpdater.h"
#import "Cryptography.h"
#import "OWSFakeContactsManager.h"
#import "OWSFakeContactsUpdater.h"
#import "OWSFakeNetworkManager.h"
#import "OWSMessageSender.h"
#import "OWSSignalServiceProtos.pb.h"
#import "TSGroupThread.h"
#import "TSMessagesManager.h"
#import "TSNetworkManager.h"
#import "TSStorageManager.h"
#import "objc/runtime.h"
@interface TSMessagesManagerTest : XCTestCase
@end
NS_ASSUME_NONNULL_BEGIN
@interface TSMessagesManager (Testing)
@ -23,121 +24,65 @@
- (void)handleIncomingEnvelope:(OWSSignalServiceProtosEnvelope *)messageEnvelope
withSyncMessage:(OWSSignalServiceProtosSyncMessage *)syncMessage;
// private method we are stubbing via swizzle.
- (BOOL)uploadDataWithProgress:(NSData *)cipherText location:(NSString *)location attachmentID:(NSString *)attachmentID;
- (void)handleIncomingEnvelope:(OWSSignalServiceProtosEnvelope *)messageEnvelope
withDataMessage:(OWSSignalServiceProtosDataMessage *)dataMessage;
@end
@implementation TSMessagesManager (Testing)
@interface OWSFakeMessageSender : OWSMessageSender
+ (void)swapOriginalSelector:(SEL)originalSelector replacement:(SEL)replacementSelector
{
Class class = [self class];
Method originalMethod = class_getInstanceMethod(class, originalSelector);
Method replacementMethod = class_getInstanceMethod(class, replacementSelector);
// When swizzling a class method, use the following:
// Class class = object_getClass((id)self);
// ...
// Method originalMethod = class_getClassMethod(class, originalSelector);
// Method swizzledMethod = class_getClassMethod(class, swizzledSelector);
BOOL didAddMethod = class_addMethod(class,
originalSelector,
method_getImplementation(replacementMethod),
method_getTypeEncoding(replacementMethod));
if (didAddMethod) {
class_replaceMethod(class,
replacementSelector,
method_getImplementation(originalMethod),
method_getTypeEncoding(originalMethod));
} else {
method_exchangeImplementations(originalMethod, replacementMethod);
}
}
+ (void)load
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[self swapOriginalSelector:@selector(uploadDataWithProgress:location:attachmentID:)
replacement:@selector(stubbedUploadDataWithProgress:location:attachmentID:)];
[self swapOriginalSelector:@selector(deviceMessages:forRecipient:inThread:)
replacement:@selector(stubbedDeviceMessages:forRecipient:inThread:)];
});
}
#pragma mark - Method Swizzling
- (BOOL)stubbedUploadDataWithProgress:(NSData *)cipherText
location:(NSString *)location
attachmentID:(NSString *)attachmentID
{
NSLog(@"Faking successful upload.");
return YES;
}
- (NSArray<NSDictionary *> *)stubbedDeviceMessages:(TSOutgoingMessage *)message
forRecipient:(SignalRecipient *)recipient
inThread:(TSThread *)thread
{
// Upon originally provisioning, we won't have a device to send to.
NSLog(@"Stubbed device message to return empty list.");
return @[];
}
@property (nonatomic, readonly) XCTestExpectation *expectation;
@end
@interface OWSTSMessagesManagerTestNetworkManager : TSNetworkManager
- (instancetype)initWithExpectation:(XCTestExpectation *)messageWasSubmitted;
@property XCTestExpectation *expectation;
@end
@implementation OWSTSMessagesManagerTestNetworkManager
@implementation OWSFakeMessageSender
- (instancetype)initWithExpectation:(XCTestExpectation *)expectation
{
self = [self init];
if (!self) {
return self;
}
_expectation = expectation;
return self;
}
- (void)makeRequest:(TSRequest *)request
success:(void (^)(NSURLSessionDataTask *task, id responseObject))success
failure:(void (^)(NSURLSessionDataTask *task, NSError *error))failure
- (void)sendTemporaryAttachmentData:(NSData *)attachmentData
contentType:(NSString *)contentType
inMessage:(TSOutgoingMessage *)outgoingMessage
success:(void (^)())successHandler
failure:(void (^)(NSError *error))failureHandler
{
if ([request isKindOfClass:[TSAllocAttachmentRequest class]]) {
NSDictionary *fakeResponse = @{ @"id" : @(1234), @"location" : @"fake-location" };
success(nil, fakeResponse);
} else if ([request isKindOfClass:[TSSubmitMessageRequest class]]) {
[self.expectation fulfill];
} else {
NSLog(@"Ignoring unhandled request: %@", request);
}
NSLog(@"Faking sendTemporyAttachmentData.");
[self.expectation fulfill];
successHandler();
}
@end
@interface TSMessagesManagerTest : XCTestCase
@end
@implementation TSMessagesManagerTest
- (TSMessagesManager *)messagesManagerWithSender:(OWSMessageSender *)messageSender
{
return [[TSMessagesManager alloc] initWithNetworkManager:[OWSFakeNetworkManager new]
storageManager:[TSStorageManager sharedManager]
contactsManager:[OWSFakeContactsManager new]
contactsUpdater:[OWSFakeContactsUpdater new]
messageSender:messageSender];
}
- (void)testIncomingSyncContactMessage
{
OWSFakeContactsUpdater *fakeContactsUpdater = [OWSFakeContactsUpdater new];
XCTestExpectation *messageWasSubmitted = [self expectationWithDescription:@"message was submitted"];
OWSTSMessagesManagerTestNetworkManager *fakeNetworkManager =
[[OWSTSMessagesManagerTestNetworkManager alloc] initWithExpectation:messageWasSubmitted];
OWSFakeContactsManager *fakeContactsManager = [OWSFakeContactsManager new];
XCTestExpectation *messageWasSent = [self expectationWithDescription:@"message was sent"];
TSMessagesManager *messagesManager =
[[TSMessagesManager alloc] initWithNetworkManager:fakeNetworkManager
storageManager:[TSStorageManager sharedManager]
contactsManager:fakeContactsManager
contactsUpdater:fakeContactsUpdater];
[self messagesManagerWithSender:[[OWSFakeMessageSender alloc] initWithExpectation:messageWasSent]];
OWSSignalServiceProtosEnvelopeBuilder *envelopeBuilder = [OWSSignalServiceProtosEnvelopeBuilder new];
OWSSignalServiceProtosSyncMessageBuilder *messageBuilder = [OWSSignalServiceProtosSyncMessageBuilder new];
@ -154,4 +99,67 @@
}];
}
- (void)testGroupUpdate
{
NSData *groupIdData = [Cryptography generateRandomBytes:32];
NSString *groupThreadId = [TSGroupThread threadIdFromGroupId:groupIdData];
TSGroupThread *groupThread = [TSGroupThread fetchObjectWithUniqueID:groupThreadId];
XCTAssertNil(groupThread);
TSMessagesManager *messagesManager = [self messagesManagerWithSender:[OWSFakeMessageSender new]];
OWSSignalServiceProtosEnvelopeBuilder *envelopeBuilder = [OWSSignalServiceProtosEnvelopeBuilder new];
OWSSignalServiceProtosGroupContextBuilder *groupContextBuilder = [OWSSignalServiceProtosGroupContextBuilder new];
groupContextBuilder.name = @"Newly created Group Name";
groupContextBuilder.id = groupIdData;
groupContextBuilder.type = OWSSignalServiceProtosGroupContextTypeUpdate;
OWSSignalServiceProtosDataMessageBuilder *messageBuilder = [OWSSignalServiceProtosDataMessageBuilder new];
messageBuilder.group = [groupContextBuilder build];
[messagesManager handleIncomingEnvelope:[envelopeBuilder build] withDataMessage:[messageBuilder build]];
groupThread = [TSGroupThread fetchObjectWithUniqueID:groupThreadId];
XCTAssertNotNil(groupThread);
XCTAssertEqualObjects(@"Newly created Group Name", groupThread.name);
}
- (void)testGroupUpdateWithAvatar
{
NSData *groupIdData = [Cryptography generateRandomBytes:32];
NSString *groupThreadId = [TSGroupThread threadIdFromGroupId:groupIdData];
TSGroupThread *groupThread = [TSGroupThread fetchObjectWithUniqueID:groupThreadId];
XCTAssertNil(groupThread);
TSMessagesManager *messagesManager = [self messagesManagerWithSender:[OWSFakeMessageSender new]];
OWSSignalServiceProtosEnvelopeBuilder *envelopeBuilder = [OWSSignalServiceProtosEnvelopeBuilder new];
OWSSignalServiceProtosGroupContextBuilder *groupContextBuilder = [OWSSignalServiceProtosGroupContextBuilder new];
groupContextBuilder.name = @"Newly created Group with Avatar Name";
groupContextBuilder.id = groupIdData;
groupContextBuilder.type = OWSSignalServiceProtosGroupContextTypeUpdate;
OWSSignalServiceProtosAttachmentPointerBuilder *attachmentBuilder =
[OWSSignalServiceProtosAttachmentPointerBuilder new];
attachmentBuilder.id = 1234;
attachmentBuilder.contentType = @"image/png";
attachmentBuilder.key = [NSData new];
attachmentBuilder.size = 123;
groupContextBuilder.avatar = [attachmentBuilder build];
OWSSignalServiceProtosDataMessageBuilder *messageBuilder = [OWSSignalServiceProtosDataMessageBuilder new];
messageBuilder.group = [groupContextBuilder build];
[messagesManager handleIncomingEnvelope:[envelopeBuilder build] withDataMessage:[messageBuilder build]];
groupThread = [TSGroupThread fetchObjectWithUniqueID:groupThreadId];
XCTAssertNotNil(groupThread);
XCTAssertEqualObjects(@"Newly created Group with Avatar Name", groupThread.name);
}
@end
NS_ASSUME_NONNULL_END

View File

@ -71,11 +71,9 @@
- (void)testFilesWithoutInteractionsAreDeleted
{
TSAttachmentStream *attachmentStream = [[TSAttachmentStream alloc] initWithIdentifier:@"orphaned-attachment"
data:[NSData new]
key:[NSData new]
contentType:@"image/jpeg"];
NSError *error;
TSAttachmentStream *attachmentStream = [[TSAttachmentStream alloc] initWithContentType:@"image/jpeg"];
[attachmentStream writeData:[NSData new] error:&error];
[attachmentStream save];
NSString *orphanedFilePath = [attachmentStream filePath];
BOOL fileExists = [[NSFileManager defaultManager] fileExistsAtPath:orphanedFilePath];
@ -93,10 +91,9 @@
TSContactThread *savedThread = [[TSContactThread alloc] initWithUniqueId:@"this-thread-exists"];
[savedThread save];
TSAttachmentStream *attachmentStream = [[TSAttachmentStream alloc] initWithIdentifier:@"legit-attachment"
data:[NSData new]
key:[NSData new]
contentType:@"image/jpeg"];
NSError *error;
TSAttachmentStream *attachmentStream = [[TSAttachmentStream alloc] initWithContentType:@"image/jpeg"];
[attachmentStream writeData:[NSData new] error:&error];
[attachmentStream save];
TSIncomingMessage *incomingMessage = [[TSIncomingMessage alloc] initWithTimestamp:1
@ -120,15 +117,12 @@
- (void)testFilesWithoutAttachmentStreamsAreDeleted
{
TSAttachmentStream *attachmentStream = [[TSAttachmentStream alloc] initWithIdentifier:@"orphaned-attachment"
data:[NSData new]
key:[NSData new]
contentType:@"image/jpeg"];
NSError *error;
TSAttachmentStream *attachmentStream = [[TSAttachmentStream alloc] initWithContentType:@"image/jpeg"];
[attachmentStream writeData:[NSData new] error:&error];
// Intentionally not saved, because we want a lingering file.
// This relies on a bug(?) in the current TSAttachmentStream init implementation where the file is created during
// `init` rather than during `save`. If that bug is fixed, we'll have to update this test to manually create the
// file to set up the correct initial state.
NSString *orphanedFilePath = [attachmentStream filePath];
BOOL fileExists = [[NSFileManager defaultManager] fileExistsAtPath:orphanedFilePath];
XCTAssert(fileExists);

View File

@ -119,50 +119,41 @@
- (void)testMessagesDeletedOnThreadDeletion
{
uint64_t timestamp = 666;
NSString *body = @"A child born today will grow up with no conception of privacy at all. Theyll never know what it means to have a private moment to themselves an unrecorded, unanalyzed thought. And thats a problem because privacy matters; privacy is what allows us to determine who we are and who we want to be.";
for (uint64_t i = timestamp; i<100; i++) {
NSMutableArray<TSIncomingMessage *> *messages = [NSMutableArray new];
for (int i = 0; i < 10; i++) {
TSIncomingMessage *newMessage = [[TSIncomingMessage alloc] initWithTimestamp:i
inThread:self.thread
authorId:[self.thread contactIdentifier]
messageBody:body];
[messages addObject:newMessage];
[newMessage save];
}
[[TSStorageManager sharedManager].dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
for (uint64_t i = timestamp; i<100; i++) {
TSIncomingMessage *fetchedMessage = [TSIncomingMessage fetchObjectWithUniqueID:[TSInteraction stringFromTimeStamp:timestamp] transaction:transaction];
NSAssert([fetchedMessage.body isEqualToString:body], @"Body of incoming message recovered");
NSAssert(fetchedMessage.attachmentIds == nil, @"attachments are nil");
NSAssert([fetchedMessage.uniqueId isEqualToString:[TSInteraction stringFromTimeStamp:timestamp]], @"Unique identifier is accurate");
NSAssert(fetchedMessage.wasRead == false, @"Message should originally be unread");
NSAssert([fetchedMessage.uniqueThreadId isEqualToString:self.thread.uniqueId], @"Isn't stored in the right thread!");
}
}];
for (TSIncomingMessage *message in messages) {
TSIncomingMessage *fetchedMessage = [TSIncomingMessage fetchObjectWithUniqueID:message.uniqueId];
XCTAssertEqualObjects(fetchedMessage.body, body, @"Body of incoming message recovered");
XCTAssertEqual(0, fetchedMessage.attachmentIds.count, @"attachments are nil");
XCTAssertEqualObjects(fetchedMessage.uniqueId, message.uniqueId, @"Unique identifier is accurate");
XCTAssertFalse(fetchedMessage.wasRead, @"Message should originally be unread");
XCTAssertEqualObjects(
fetchedMessage.uniqueThreadId, self.thread.uniqueId, @"Isn't stored in the right thread!");
}
[self.thread remove];
[[TSStorageManager sharedManager].dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
for (uint64_t i = timestamp; i<100; i++) {
TSIncomingMessage *fetchedMessage = [TSIncomingMessage fetchObjectWithUniqueID:[TSInteraction stringFromTimeStamp:timestamp] transaction:transaction];
NSAssert(fetchedMessage == nil, @"Message should be deleted!");
}
}];
for (TSIncomingMessage *message in messages) {
TSIncomingMessage *fetchedMessage = [TSIncomingMessage fetchObjectWithUniqueID:message.uniqueId];
XCTAssertNil(fetchedMessage, @"Message should be deleted!");
}
}
- (void)testGroupMessagesDeletedOnThreadDeletion
{
uint64_t timestamp = 666;
NSString *body = @"A child born today will grow up with no conception of privacy at all. Theyll never know what it means to have a private moment to themselves an unrecorded, unanalyzed thought. And thats a problem because privacy matters; privacy is what allows us to determine who we are and who we want to be.";
TSAttachmentStream *pointer = [[TSAttachmentStream alloc] initWithIdentifier:@"helloid" data:[Cryptography generateRandomBytes:16] key:[Cryptography generateRandomBytes:16] contentType:@"data/random"];
__block TSGroupThread *thread;
[[TSStorageManager sharedManager].dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
@ -173,50 +164,37 @@
transaction:transaction];
[thread saveWithTransaction:transaction];
[pointer saveWithTransaction:transaction];
}];
TSStorageManager *manager = [TSStorageManager sharedManager];
[manager purgeCollection:[TSMessage collection]];
for (uint64_t i = timestamp; i<100; i++) {
NSMutableArray<TSIncomingMessage *> *messages = [NSMutableArray new];
for (uint64_t i = 0; i < 10; i++) {
TSIncomingMessage *newMessage = [[TSIncomingMessage alloc] initWithTimestamp:i
inThread:thread
authorId:@"Ed"
messageBody:body];
[newMessage save];
[messages addObject:newMessage];
}
for (TSIncomingMessage *message in messages) {
TSIncomingMessage *fetchedMessage = [TSIncomingMessage fetchObjectWithUniqueID:message.uniqueId];
XCTAssertNotNil(fetchedMessage);
XCTAssertEqualObjects(fetchedMessage.body, body, @"Body of incoming message recovered");
XCTAssertEqual(0, fetchedMessage.attachmentIds.count, @"attachments are empty");
XCTAssertEqualObjects(fetchedMessage.uniqueId, message.uniqueId, @"Unique identifier is accurate");
XCTAssertFalse(fetchedMessage.wasRead, @"Message should originally be unread");
XCTAssertEqualObjects(fetchedMessage.uniqueThreadId, thread.uniqueId, @"Isn't stored in the right thread!");
}
[thread remove];
for (TSIncomingMessage *message in messages) {
TSIncomingMessage *fetchedMessage = [TSIncomingMessage fetchObjectWithUniqueID:message.uniqueId];
XCTAssertNil(fetchedMessage, @"Message should be deleted!");
}
[[TSStorageManager sharedManager].dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
for (uint64_t i = timestamp; i<100; i++) {
TSIncomingMessage *fetchedMessage = [TSIncomingMessage fetchObjectWithUniqueID:[TSInteraction stringFromTimeStamp:timestamp] transaction:transaction];
TSAttachmentStream *fetchedPointer = [TSAttachmentStream fetchObjectWithUniqueID:pointer.uniqueId];
NSAssert([fetchedPointer.image isEqual:pointer.image], @"attachment pointers not equal");
NSAssert([fetchedMessage.body isEqualToString:body], @"Body of incoming message recovered");
NSAssert(fetchedMessage.attachmentIds == nil, @"attachments are nil");
NSAssert([fetchedMessage.uniqueId isEqualToString:[TSInteraction stringFromTimeStamp:timestamp]], @"Unique identifier is accurate");
NSAssert(fetchedMessage.wasRead == false, @"Message should originally be unread");
NSAssert([fetchedMessage.uniqueThreadId isEqualToString:self.thread.uniqueId], @"Isn't stored in the right thread!");
}
}];
[self.thread remove];
[[TSStorageManager sharedManager].dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
for (uint64_t i = timestamp; i<100; i++) {
TSIncomingMessage *fetchedMessage = [TSIncomingMessage fetchObjectWithUniqueID:[TSInteraction stringFromTimeStamp:timestamp] transaction:transaction];
TSAttachmentStream *fetchedPointer = [TSAttachmentStream fetchObjectWithUniqueID:pointer.uniqueId];
NSAssert(fetchedPointer == nil, @"Attachment pointer should be deleted");
NSAssert(fetchedMessage == nil, @"Message should be deleted!");
}
}];
}
@end

View File

@ -8,14 +8,10 @@ NS_ASSUME_NONNULL_BEGIN
@implementation OWSFakeContactsUpdater
- (void)synchronousLookup:(NSString *)identifier
success:(void (^)(SignalRecipient *))success
failure:(void (^)(NSError *error))failure
- (SignalRecipient *)synchronousLookup:(NSString *)identifier error:(NSError **)error
{
NSLog(@"[OWSFakeContactsUpdater] Faking contact lookup.");
SignalRecipient *fakeRecipient =
[[SignalRecipient alloc] initWithTextSecureIdentifier:@"fake-recipient-id" relay:nil supportsVoice:YES];
success(fakeRecipient);
return [[SignalRecipient alloc] initWithTextSecureIdentifier:@"fake-recipient-id" relay:nil supportsVoice:YES];
}
@end

View File

@ -0,0 +1,12 @@
// Created by Michael Kirk on 10/19/16.
// Copyright © 2016 Open Whisper Systems. All rights reserved.
#import "TSNetworkManager.h"
NS_ASSUME_NONNULL_BEGIN
@interface OWSFakeNetworkManager : TSNetworkManager
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,19 @@
// Created by Michael Kirk on 10/19/16.
// Copyright © 2016 Open Whisper Systems. All rights reserved.
#import "OWSFakeNetworkManager.h"
NS_ASSUME_NONNULL_BEGIN
@implementation OWSFakeNetworkManager
- (void)makeRequest:(TSRequest *)request
success:(void (^)(NSURLSessionDataTask *task, id responseObject))success
failure:(void (^)(NSURLSessionDataTask *task, NSError *error))failure
{
NSLog(@"[OWSFakeNetworkManager] Ignoring unhandled request: %@", request);
}
@end
NS_ASSUME_NONNULL_END