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:
parent
d4c55d6940
commit
4ba1e86ec1
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
16
Makefile
16
Makefile
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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:@""]) {
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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];
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -9,6 +9,8 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
* Abstract message class.
|
||||
*/
|
||||
|
||||
@class TSAttachmentPointer;
|
||||
|
||||
typedef NS_ENUM(NSInteger, TSGroupMetaMessage) {
|
||||
TSGroupMessageNone,
|
||||
TSGroupMessageNew,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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);
|
||||
}];
|
||||
}
|
||||
|
||||
|
|
|
@ -23,5 +23,4 @@
|
|||
- (NSString *)getInfoStringAboutUpdateTo:(TSGroupModel *)model contactsManager:(id<ContactsManagerProtocol>)contactsManager;
|
||||
#endif
|
||||
|
||||
|
||||
@end
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -10,6 +10,6 @@
|
|||
|
||||
@interface TSAttachmentRequest : TSRequest
|
||||
|
||||
- (TSRequest *)initWithId:(NSNumber *)attachmentId relay:(NSString *)relay;
|
||||
- (TSRequest *)initWithId:(UInt64)attachmentId relay:(NSString *)relay;
|
||||
|
||||
@end
|
||||
|
|
|
@ -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];
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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];
|
||||
}];
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface OWSDispatch : NSObject
|
||||
|
||||
+ (dispatch_queue_t)attachmentsQueue;
|
||||
|
||||
+ (dispatch_queue_t)sendingQueue;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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. They’ll never know what it means to have a private moment to themselves an unrecorded, unanalyzed thought. And that’s 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. They’ll never know what it means to have a private moment to themselves an unrecorded, unanalyzed thought. And that’s 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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
Loading…
Reference in New Issue