diff --git a/Example/TSKitiOSTestApp/TSKitiOSTestApp.xcodeproj/project.pbxproj b/Example/TSKitiOSTestApp/TSKitiOSTestApp.xcodeproj/project.pbxproj index 3ac6a8eab..46206806b 100644 --- a/Example/TSKitiOSTestApp/TSKitiOSTestApp.xcodeproj/project.pbxproj +++ b/Example/TSKitiOSTestApp/TSKitiOSTestApp.xcodeproj/project.pbxproj @@ -37,6 +37,7 @@ 45B840211D988DA100F9E938 /* OWSReadReceiptTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 45B840201D988DA100F9E938 /* OWSReadReceiptTest.m */; }; 45C6A09A1D2F029B007D8AC0 /* TSMessageTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 45C6A0991D2F029B007D8AC0 /* TSMessageTest.m */; }; 45D7243F1D67899F00E0CA54 /* OWSDeviceProvisionerTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 45D7243E1D67899F00E0CA54 /* OWSDeviceProvisionerTest.m */; }; + 45E741B61E5D14E800735842 /* OWSIncomingMessageFinderTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 45E741B51E5D14E800735842 /* OWSIncomingMessageFinderTest.m */; }; 51520592F83F2440F2DE4D67 /* libPods-TSKitiOSTestApp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = B8362AB8E280E0F64352F08A /* libPods-TSKitiOSTestApp.a */; }; 6323E1F7730289398452E5C5 /* OWSFingerprintTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 6323E02A33682A8838FE3F27 /* OWSFingerprintTest.m */; }; 6323E339D5B8F4CB77EB3ED4 /* SignalRecipientTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 6323E3E540CF763D71DACB59 /* SignalRecipientTest.m */; }; @@ -99,6 +100,7 @@ 45B840201D988DA100F9E938 /* OWSReadReceiptTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = OWSReadReceiptTest.m; path = ../../../tests/Devices/OWSReadReceiptTest.m; sourceTree = ""; }; 45C6A0991D2F029B007D8AC0 /* TSMessageTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = TSMessageTest.m; path = ../../../tests/Messages/Interactions/TSMessageTest.m; sourceTree = ""; }; 45D7243E1D67899F00E0CA54 /* OWSDeviceProvisionerTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = OWSDeviceProvisionerTest.m; path = ../../../tests/Devices/OWSDeviceProvisionerTest.m; sourceTree = ""; }; + 45E741B51E5D14E800735842 /* OWSIncomingMessageFinderTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = OWSIncomingMessageFinderTest.m; path = ../../../tests/Messages/OWSIncomingMessageFinderTest.m; sourceTree = ""; }; 6323E02A33682A8838FE3F27 /* OWSFingerprintTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = OWSFingerprintTest.m; path = ../../../tests/Security/OWSFingerprintTest.m; sourceTree = ""; }; 6323E3E540CF763D71DACB59 /* SignalRecipientTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = SignalRecipientTest.m; path = ../../tests/Contacts/SignalRecipientTest.m; sourceTree = ""; }; B6273DD11C13A2E500738558 /* TSKitiOSTestApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = TSKitiOSTestApp.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -237,6 +239,7 @@ 45046FDF1D95A6130015EFF2 /* TSMessagesManagerTest.m */, 454021EC1D960ABF00F2126D /* OWSDisappearingMessageFinderTest.m */, 453E1FCE1DA8313100DDD7B7 /* OWSMessageSenderTest.m */, + 45E741B51E5D14E800735842 /* OWSIncomingMessageFinderTest.m */, ); name = Messages; sourceTree = ""; @@ -561,6 +564,7 @@ 45D7243F1D67899F00E0CA54 /* OWSDeviceProvisionerTest.m in Sources */, 4516E3E81DD153CC00DC4206 /* TSGroupThreadTest.m in Sources */, 45458B791CC342B600A02153 /* TSStoragePreKeyStoreTests.m in Sources */, + 45E741B61E5D14E800735842 /* OWSIncomingMessageFinderTest.m in Sources */, 452EE6D51D4AC43300E934BA /* OWSOrphanedDataCleanerTest.m in Sources */, 450E3C9A1D96DD2600BF4EB6 /* OWSDisappearingMessagesJobTest.m in Sources */, 452EE6CF1D4A754C00E934BA /* TSThreadTest.m in Sources */, diff --git a/src/Devices/OWSDevice.h b/src/Devices/OWSDevice.h index 774faa521..5caf78e4b 100644 --- a/src/Devices/OWSDevice.h +++ b/src/Devices/OWSDevice.h @@ -1,10 +1,14 @@ -// Copyright © 2016 Open Whisper Systems. All rights reserved. +// +// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// #import "TSYapDatabaseObject.h" #import NS_ASSUME_NONNULL_BEGIN +extern uint32_t const OWSDevicePrimaryDeviceId; + @interface OWSDevice : TSYapDatabaseObject @property (nonatomic, readonly) NSInteger deviceId; @@ -22,6 +26,11 @@ NS_ASSUME_NONNULL_BEGIN */ + (void)replaceAll:(NSArray *)devices; +/** + * The id of the device currently running this application + */ ++ (uint32_t)currentDeviceId; + /** * * @param transaction diff --git a/src/Devices/OWSDevice.m b/src/Devices/OWSDevice.m index a8f710ff1..3f3e9ba6b 100644 --- a/src/Devices/OWSDevice.m +++ b/src/Devices/OWSDevice.m @@ -1,4 +1,6 @@ -// Copyright © 2016 Open Whisper Systems. All rights reserved. +// +// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// #import "OWSDevice.h" #import "NSDate+millisecondTimeStamp.h" @@ -10,7 +12,7 @@ NS_ASSUME_NONNULL_BEGIN static MTLValueTransformer *_millisecondTimestampToDateTransformer; -static int const OWSDevicePrimaryDeviceId = 1; +uint32_t const OWSDevicePrimaryDeviceId = 1; @interface OWSDevice () @@ -108,6 +110,13 @@ static int const OWSDevicePrimaryDeviceId = 1; return _millisecondTimestampToDateTransformer; } ++ (uint32_t)currentDeviceId +{ + // Someday it may be possible to have a non-primary iOS device, but for now + // any iOS device must be the primary device. + return OWSDevicePrimaryDeviceId; +} + - (BOOL)isPrimaryDevice { return self.deviceId == OWSDevicePrimaryDeviceId; diff --git a/src/Messages/Interactions/TSIncomingMessage.h b/src/Messages/Interactions/TSIncomingMessage.h index f7804ea2b..cb1b43d83 100644 --- a/src/Messages/Interactions/TSIncomingMessage.h +++ b/src/Messages/Interactions/TSIncomingMessage.h @@ -23,6 +23,8 @@ extern NSString *const TSIncomingMessageWasReadOnThisDeviceNotification; * Thread to which the message belongs * @param authorId * Signal ID (i.e. e164) of the user who sent the message + * @param sourceDeviceId + * Numeric ID of the device used to send the message. Used to detect duplicate messages. * @param body * Body of the message * @@ -31,30 +33,9 @@ extern NSString *const TSIncomingMessageWasReadOnThisDeviceNotification; - (instancetype)initWithTimestamp:(uint64_t)timestamp inThread:(TSThread *)thread authorId:(NSString *)authorId + sourceDeviceId:(uint32_t)sourceDeviceId messageBody:(nullable NSString *)body; -/** - * Inits an incoming group message with attachments - * - * @param timestamp - * When the message was created in milliseconds since epoch - * @param thread - * Thread to which the message belongs - * @param authorId - * Signal ID (i.e. e164) of the user who sent the message - * @param body - * Body of the message - * @param attachmentIds - * The uniqueIds for the message's attachments - * - * @return initiated incoming group message - */ -- (instancetype)initWithTimestamp:(uint64_t)timestamp - inThread:(TSThread *)thread - authorId:(NSString *)authorId - messageBody:(nullable NSString *)body - attachmentIds:(NSArray *)attachmentIds; - /** * Inits an incoming group message that expires. * @@ -64,6 +45,8 @@ extern NSString *const TSIncomingMessageWasReadOnThisDeviceNotification; * Thread to which the message belongs * @param authorId * Signal ID (i.e. e164) of the user who sent the message + * @param sourceDeviceId + * Numeric ID of the device used to send the message. Used to detect duplicate messages. * @param body * Body of the message * @param attachmentIds @@ -76,6 +59,7 @@ extern NSString *const TSIncomingMessageWasReadOnThisDeviceNotification; - (instancetype)initWithTimestamp:(uint64_t)timestamp inThread:(TSThread *)thread authorId:(NSString *)authorId + sourceDeviceId:(uint32_t)sourceDeviceId messageBody:(nullable NSString *)body attachmentIds:(NSArray *)attachmentIds expiresInSeconds:(uint32_t)expiresInSeconds NS_DESIGNATED_INITIALIZER; @@ -84,8 +68,8 @@ extern NSString *const TSIncomingMessageWasReadOnThisDeviceNotification; /** - * For sake of a smaller API, you must specify an author id for all incoming messages - * though we technically could get the author id from a contact thread. + * For sake of a smaller API, and simplifying assumptions elsewhere, you must specify an author id for *all* incoming + * messages, even though we technically could infer the author id for a contact thread. */ - (instancetype)initWithTimestamp:(uint64_t)timestamp NS_UNAVAILABLE; - (instancetype)initWithTimestamp:(uint64_t)timestamp inThread:(nullable TSThread *)thread NS_UNAVAILABLE; @@ -120,6 +104,9 @@ extern NSString *const TSIncomingMessageWasReadOnThisDeviceNotification; + (nullable instancetype)findMessageWithAuthorId:(NSString *)authorId timestamp:(uint64_t)timestamp; @property (nonatomic, readonly) NSString *authorId; + +// This will be 0 for messages created before we were tracking sourceDeviceId +@property (nonatomic, readonly) UInt32 sourceDeviceId; @property (nonatomic, readonly, getter=wasRead) BOOL read; /* diff --git a/src/Messages/Interactions/TSIncomingMessage.m b/src/Messages/Interactions/TSIncomingMessage.m index e10acedc8..c91ea7cce 100644 --- a/src/Messages/Interactions/TSIncomingMessage.m +++ b/src/Messages/Interactions/TSIncomingMessage.m @@ -22,28 +22,22 @@ NSString *const TSIncomingMessageWasReadOnThisDeviceNotification = @"TSIncomingM - (instancetype)initWithTimestamp:(uint64_t)timestamp inThread:(TSThread *)thread authorId:(NSString *)authorId + sourceDeviceId:(uint32_t)sourceDeviceId messageBody:(nullable NSString *)body -{ - return [self initWithTimestamp:timestamp inThread:thread authorId:authorId messageBody:body attachmentIds:@[]]; -} - -- (instancetype)initWithTimestamp:(uint64_t)timestamp - inThread:(TSThread *)thread - authorId:(NSString *)authorId - messageBody:(nullable NSString *)body - attachmentIds:(NSArray *)attachmentIds { return [self initWithTimestamp:timestamp inThread:thread authorId:authorId + sourceDeviceId:sourceDeviceId messageBody:body - attachmentIds:attachmentIds + attachmentIds:@[] expiresInSeconds:0]; } - (instancetype)initWithTimestamp:(uint64_t)timestamp inThread:(TSThread *)thread authorId:(NSString *)authorId + sourceDeviceId:(uint32_t)sourceDeviceId messageBody:(nullable NSString *)body attachmentIds:(NSArray *)attachmentIds expiresInSeconds:(uint32_t)expiresInSeconds @@ -60,6 +54,7 @@ NSString *const TSIncomingMessageWasReadOnThisDeviceNotification = @"TSIncomingM } _authorId = authorId; + _sourceDeviceId = sourceDeviceId; _read = NO; OWSAssert(self.receivedAtDate); diff --git a/src/Messages/OWSMessageSender.m b/src/Messages/OWSMessageSender.m index 4705c0960..01d3f7f95 100644 --- a/src/Messages/OWSMessageSender.m +++ b/src/Messages/OWSMessageSender.m @@ -5,6 +5,7 @@ #import "OWSMessageSender.h" #import "ContactsUpdater.h" #import "NSData+messagePadding.h" +#import "OWSDevice.h" #import "OWSDisappearingMessagesJob.h" #import "OWSError.h" #import "OWSLegacyMessageServiceParams.h" @@ -622,6 +623,7 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException"; [[TSIncomingMessage alloc] initWithTimestamp:(outgoingMessage.timestamp + 1) inThread:cThread authorId:[cThread contactIdentifier] + sourceDeviceId:[OWSDevice currentDeviceId] messageBody:outgoingMessage.body attachmentIds:outgoingMessage.attachmentIds expiresInSeconds:outgoingMessage.expiresInSeconds]; diff --git a/src/Messages/TSMessagesManager.m b/src/Messages/TSMessagesManager.m index 28098f572..3daad6d44 100644 --- a/src/Messages/TSMessagesManager.m +++ b/src/Messages/TSMessagesManager.m @@ -15,6 +15,7 @@ #import "OWSDisappearingMessagesConfiguration.h" #import "OWSDisappearingMessagesJob.h" #import "OWSError.h" +#import "OWSIncomingMessageFinder.h" #import "OWSIncomingSentMessageTranscript.h" #import "OWSMessageSender.h" #import "OWSReadReceiptsProcessor.h" @@ -46,6 +47,7 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, readonly) TSStorageManager *storageManager; @property (nonatomic, readonly) OWSMessageSender *messageSender; @property (nonatomic, readonly) OWSDisappearingMessagesJob *disappearingMessagesJob; +@property (nonatomic, readonly) OWSIncomingMessageFinder *incomingMessageFinder; @end @@ -102,6 +104,7 @@ NS_ASSUME_NONNULL_BEGIN _dbConnection = storageManager.newDatabaseConnection; _disappearingMessagesJob = [[OWSDisappearingMessagesJob alloc] initWithStorageManager:storageManager]; + _incomingMessageFinder = [[OWSIncomingMessageFinder alloc] initWithDatabase:storageManager.database]; return self; } @@ -281,6 +284,18 @@ NS_ASSUME_NONNULL_BEGIN - (void)handleEnvelope:(OWSSignalServiceProtosEnvelope *)envelope plaintextData:(NSData *)plaintextData { OWSAssert([NSThread isMainThread]); + OWSAssert(envelope.hasTimestamp && envelope.timestamp > 0); + OWSAssert(envelope.hasSource && envelope.source.length > 0); + OWSAssert(envelope.hasSourceDevice && envelope.sourceDevice > 0); + + BOOL duplicateEnvelope = [self.incomingMessageFinder existsMessageWithTimestamp:envelope.timestamp + sourceId:envelope.source + sourceDeviceId:envelope.sourceDevice]; + if (duplicateEnvelope) { + DDLogInfo(@"%@ Ignoring previously received envelope with timestamp: %llu", self.tag, envelope.timestamp); + return; + } + if (envelope.hasContent) { OWSSignalServiceProtosContent *content = [OWSSignalServiceProtosContent parseFromData:plaintextData]; if (content.hasSyncMessage) { @@ -290,7 +305,7 @@ NS_ASSUME_NONNULL_BEGIN } else if (content.hasCallMessage) { [self handleIncomingEnvelope:envelope withCallMessage:content.callMessage]; } else { - DDLogWarn(@"%@ Ignoring envelope.Content with no known payload", self.tag); + DDLogWarn(@"%@ Ignoring envelope. Content with no known payload", self.tag); } } else if (envelope.hasLegacyMessage) { // DEPRECATED - Remove after all clients have been upgraded. OWSSignalServiceProtosDataMessage *dataMessage = @@ -611,6 +626,7 @@ NS_ASSUME_NONNULL_BEGIN incomingMessage = [[TSIncomingMessage alloc] initWithTimestamp:timestamp inThread:gThread authorId:envelope.source + sourceDeviceId:envelope.sourceDevice messageBody:body attachmentIds:attachmentIds expiresInSeconds:dataMessage.expireTimer]; @@ -632,6 +648,7 @@ NS_ASSUME_NONNULL_BEGIN incomingMessage = [[TSIncomingMessage alloc] initWithTimestamp:timestamp inThread:cThread authorId:[cThread contactIdentifier] + sourceDeviceId:envelope.sourceDevice messageBody:body attachmentIds:attachmentIds expiresInSeconds:dataMessage.expireTimer]; @@ -648,25 +665,18 @@ NS_ASSUME_NONNULL_BEGIN [incomingMessage markAsReadLocallyWithTransaction:transaction]; } - // Android allows attachments to be sent with body. + // Other clients allow attachments to be sent along with body, we want the text displayed as a separate + // message if ([attachmentIds count] > 0 && body != nil && ![body isEqualToString:@""]) { // We want the text to be displayed under the attachment uint64_t textMessageTimestamp = timestamp + 1; - TSIncomingMessage *textMessage; - if ([thread isGroupThread]) { - TSGroupThread *gThread = (TSGroupThread *)thread; - textMessage = [[TSIncomingMessage alloc] initWithTimestamp:textMessageTimestamp - inThread:gThread - authorId:envelope.source - messageBody:body]; - } else { - TSContactThread *cThread = (TSContactThread *)thread; - textMessage = [[TSIncomingMessage alloc] initWithTimestamp:textMessageTimestamp - inThread:cThread - authorId:[cThread contactIdentifier] - messageBody:body]; - } - textMessage.expiresInSeconds = dataMessage.expireTimer; + TSIncomingMessage *textMessage = [[TSIncomingMessage alloc] initWithTimestamp:textMessageTimestamp + inThread:thread + authorId:envelope.source + sourceDeviceId:envelope.sourceDevice + messageBody:body + attachmentIds:@[] + expiresInSeconds:dataMessage.expireTimer]; [textMessage saveWithTransaction:transaction]; } } diff --git a/src/Storage/OWSIncomingMessageFinder.h b/src/Storage/OWSIncomingMessageFinder.h new file mode 100644 index 000000000..49c16c5de --- /dev/null +++ b/src/Storage/OWSIncomingMessageFinder.h @@ -0,0 +1,28 @@ +// +// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// + +NS_ASSUME_NONNULL_BEGIN + +@class YapDatabase; +@class YapDatabaseReadTransaction; + +@interface OWSIncomingMessageFinder : NSObject + +- (instancetype)initWithDatabase:(YapDatabase *)database NS_DESIGNATED_INITIALIZER; + +/** + * Must be called before using this finder. + */ +- (void)asyncRegisterExtension; + +/** + * Detects existance of a duplicate incoming message. + */ +- (BOOL)existsMessageWithTimestamp:(uint64_t)timestamp + sourceId:(NSString *)sourceId + sourceDeviceId:(uint32_t)sourceDeviceId; + +@end + +NS_ASSUME_NONNULL_END diff --git a/src/Storage/OWSIncomingMessageFinder.m b/src/Storage/OWSIncomingMessageFinder.m new file mode 100644 index 000000000..af28990de --- /dev/null +++ b/src/Storage/OWSIncomingMessageFinder.m @@ -0,0 +1,159 @@ +// +// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// + +#import "OWSIncomingMessageFinder.h" +#import "TSIncomingMessage.h" +#import "TSStorageManager.h" +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +NSString *const OWSIncomingMessageFinderExtensionName = @"OWSIncomingMessageFinderExtensionName"; + +NSString *const OWSIncomingMessageFinderColumnTimestamp = @"OWSIncomingMessageFinderColumnTimestamp"; +NSString *const OWSIncomingMessageFinderColumnSourceId = @"OWSIncomingMessageFinderColumnSourceId"; +NSString *const OWSIncomingMessageFinderColumnSourceDeviceId = @"OWSIncomingMessageFinderColumnSourceDeviceId"; + +@interface OWSIncomingMessageFinder () + +@property (nonatomic, readonly) YapDatabase *database; +@property (nonatomic, readonly) YapDatabaseConnection *dbConnection; + +@end + +@implementation OWSIncomingMessageFinder + +@synthesize dbConnection = _dbConnection; + +#pragma mark - init + +- (instancetype)init +{ + OWSAssert([TSStorageManager sharedManager].database != nil); + + return [self initWithDatabase:[TSStorageManager sharedManager].database]; +} + +- (instancetype)initWithDatabase:(YapDatabase *)database +{ + self = [super init]; + if (!self) { + return self; + } + + _database = database; + + return self; +} + +#pragma mark - properties + +- (YapDatabaseConnection *)dbConnection +{ + @synchronized (self) { + if (!_dbConnection) { + _dbConnection = self.database.newConnection; + } + } + return _dbConnection; +} + +#pragma mark - YAP integration + +- (YapDatabaseSecondaryIndex *)indexExtension +{ + YapDatabaseSecondaryIndexSetup *setup = [YapDatabaseSecondaryIndexSetup new]; + + [setup addColumn:OWSIncomingMessageFinderColumnTimestamp withType:YapDatabaseSecondaryIndexTypeInteger]; + [setup addColumn:OWSIncomingMessageFinderColumnSourceId withType:YapDatabaseSecondaryIndexTypeText]; + [setup addColumn:OWSIncomingMessageFinderColumnSourceDeviceId withType:YapDatabaseSecondaryIndexTypeInteger]; + + YapDatabaseSecondaryIndexWithObjectBlock block = ^(YapDatabaseReadTransaction *transaction, + NSMutableDictionary *dict, + NSString *collection, + NSString *key, + id object) { + if ([object isKindOfClass:[TSIncomingMessage class]]) { + TSIncomingMessage *incomingMessage = (TSIncomingMessage *)object; + + [dict setObject:@(incomingMessage.timestamp) forKey:OWSIncomingMessageFinderColumnTimestamp]; + [dict setObject:incomingMessage.authorId forKey:OWSIncomingMessageFinderColumnSourceId]; + [dict setObject:@(incomingMessage.sourceDeviceId) forKey:OWSIncomingMessageFinderColumnSourceDeviceId]; + } + }; + + YapDatabaseSecondaryIndexHandler *handler = [YapDatabaseSecondaryIndexHandler withObjectBlock:block]; + + return [[YapDatabaseSecondaryIndex alloc] initWithSetup:setup handler:handler]; +} + +- (void)asyncRegisterExtension +{ + DDLogInfo(@"%@ registering async.", self.tag); + [self.database asyncRegisterExtension:self.indexExtension + withName:OWSIncomingMessageFinderExtensionName + completionBlock:^(BOOL ready) { + DDLogInfo(@"%@ finished registering async.", self.tag); + }]; +} + +// We should not normally hit this, as we should have prefer registering async, but it is useful for testing. +- (void)registerExtension +{ + DDLogError(@"%@ registering SYNC. We should prefer async when possible.", self.tag); + [self.database registerExtension:self.indexExtension withName:OWSIncomingMessageFinderExtensionName]; +} + +#pragma mark - instance methods + +- (BOOL)existsMessageWithTimestamp:(uint64_t)timestamp + sourceId:(NSString *)sourceId + sourceDeviceId:(uint32_t)sourceDeviceId +{ + if (![self.database registeredExtension:OWSIncomingMessageFinderExtensionName]) { + DDLogError(@"%@ in %s but extension is not registered", self.tag, __PRETTY_FUNCTION__); + OWSAssert(NO); + + // we should be initializing this at startup rather than have an unexpectedly slow lazy setup at random. + [self registerExtension]; + } + + NSString *queryFormat = [NSString stringWithFormat:@"WHERE %@ = ? AND %@ = ? AND %@ = ?", + OWSIncomingMessageFinderColumnTimestamp, + OWSIncomingMessageFinderColumnSourceId, + OWSIncomingMessageFinderColumnSourceDeviceId]; + // YapDatabaseQuery params must be objects + YapDatabaseQuery *query = [YapDatabaseQuery queryWithFormat:queryFormat, @(timestamp), sourceId, @(sourceDeviceId)]; + + __block NSUInteger count; + __block BOOL success; + + [self.dbConnection readWithBlock:^(YapDatabaseReadTransaction *_Nonnull transaction) { + success = [[transaction ext:OWSIncomingMessageFinderExtensionName] getNumberOfRows:&count matchingQuery:query]; + }]; + + if (!success) { + OWSAssert(NO); + return NO; + } + + return count > 0; +} + +#pragma mark - Logging + ++ (NSString *)tag +{ + return [NSString stringWithFormat:@"[%@]", self.class]; +} + +- (NSString *)tag +{ + return self.class.tag; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/src/Storage/TSStorageManager.m b/src/Storage/TSStorageManager.m index b02951112..c3d9a613d 100644 --- a/src/Storage/TSStorageManager.m +++ b/src/Storage/TSStorageManager.m @@ -7,6 +7,7 @@ #import "OWSAnalytics.h" #import "OWSDisappearingMessagesFinder.h" #import "OWSFailedMessagesJob.h" +#import "OWSIncomingMessageFinder.h" #import "OWSReadReceipt.h" #import "SignalRecipient.h" #import "TSAttachmentStream.h" @@ -197,6 +198,7 @@ static NSString *keychainDBPassAccount = @"TSDatabasePass"; [self.database registerExtension:[TSDatabaseSecondaryIndexes registerTimeStampIndex] withName:@"idx"]; // Register extensions which aren't essential for rendering threads async + [[OWSIncomingMessageFinder new] asyncRegisterExtension]; [TSDatabaseView asyncRegisterSecondaryDevicesDatabaseView]; [OWSReadReceipt asyncRegisterIndexOnSenderIdAndTimestampWithDatabase:self.database]; OWSDisappearingMessagesFinder *finder = [[OWSDisappearingMessagesFinder alloc] initWithStorageManager:self]; diff --git a/tests/Contacts/TSThreadTest.m b/tests/Contacts/TSThreadTest.m index 19936de98..4f21d770d 100644 --- a/tests/Contacts/TSThreadTest.m +++ b/tests/Contacts/TSThreadTest.m @@ -1,5 +1,8 @@ -// Copyright © 2016 Open Whisper Systems. All rights reserved. +// +// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// +#import "OWSDevice.h" #import "TSAttachmentStream.h" #import "TSContactThread.h" #import "TSIncomingMessage.h" @@ -38,6 +41,7 @@ TSIncomingMessage *incomingMessage = [[TSIncomingMessage alloc] initWithTimestamp:10000 inThread:thread authorId:@"fake-author-id" + sourceDeviceId:OWSDevicePrimaryDeviceId messageBody:@"Incoming message body"]; [incomingMessage save]; @@ -70,12 +74,13 @@ BOOL incomingFileWasCreated = [[NSFileManager defaultManager] fileExistsAtPath:[incomingAttachment filePath]]; XCTAssert(incomingFileWasCreated); - TSIncomingMessage *incomingMessage = - [[TSIncomingMessage alloc] initWithTimestamp:10000 - inThread:thread - authorId:@"fake-author-id" - messageBody:@"incoming message body" - attachmentIds:[NSMutableArray arrayWithObject:incomingAttachment.uniqueId]]; + TSIncomingMessage *incomingMessage = [[TSIncomingMessage alloc] initWithTimestamp:10000 + inThread:thread + authorId:@"fake-author-id" + sourceDeviceId:OWSDevicePrimaryDeviceId + messageBody:@"incoming message body" + attachmentIds:@[ incomingAttachment.uniqueId ] + expiresInSeconds:0]; [incomingMessage save]; TSAttachmentStream *outgoingAttachment = [[TSAttachmentStream alloc] initWithContentType:@"image/jpeg"]; @@ -86,11 +91,10 @@ BOOL outgoingFileWasCreated = [[NSFileManager defaultManager] fileExistsAtPath:[outgoingAttachment filePath]]; XCTAssert(outgoingFileWasCreated); - TSOutgoingMessage *outgoingMessage = - [[TSOutgoingMessage alloc] initWithTimestamp:10000 - inThread:thread - messageBody:@"outgoing message body" - attachmentIds:[NSMutableArray arrayWithObject:outgoingAttachment.uniqueId]]; + TSOutgoingMessage *outgoingMessage = [[TSOutgoingMessage alloc] initWithTimestamp:10000 + inThread:thread + messageBody:@"outgoing message body" + attachmentIds:@[ outgoingAttachment.uniqueId ]]; [outgoingMessage save]; // Sanity check diff --git a/tests/Messages/OWSIncomingMessageFinderTest.m b/tests/Messages/OWSIncomingMessageFinderTest.m new file mode 100644 index 000000000..ba859ad95 --- /dev/null +++ b/tests/Messages/OWSIncomingMessageFinderTest.m @@ -0,0 +1,105 @@ +// +// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// + +#import "OWSDevice.h" +#import "OWSIncomingMessageFinder.h" +#import "TSContactThread.h" +#import "TSIncomingMessage.h" +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface OWSIncomingMessageFinder (Testing) + +- (void)registerExtension; + +@end + +@interface OWSIncomingMessageFinderTest : XCTestCase + +@property (nonatomic) NSString *sourceId; +@property (nonatomic) TSThread *thread; +@property (nonatomic) OWSIncomingMessageFinder *finder; + +@end + +@implementation OWSIncomingMessageFinderTest + +- (void)setUp +{ + [super setUp]; + self.sourceId = @"some-source-id"; + self.thread = [TSContactThread getOrCreateThreadWithContactId:self.sourceId]; + self.finder = [OWSIncomingMessageFinder new]; + [self.finder registerExtension]; +} + +- (void)tearDown +{ + // Put teardown code here. This method is called after the invocation of each test method in the class. + [super tearDown]; +} + +- (void)testExistingMessages +{ + + uint64_t timestamp = 1234; + BOOL result = [self.finder existsMessageWithTimestamp:timestamp + sourceId:self.sourceId + sourceDeviceId:OWSDevicePrimaryDeviceId]; + + // Sanity check. + XCTAssertFalse(result); + + // Different timestamp + [[[TSIncomingMessage alloc] initWithTimestamp:timestamp + 1 + inThread:self.thread + authorId:self.sourceId + sourceDeviceId:OWSDevicePrimaryDeviceId + messageBody:@"foo"] save]; + result = [self.finder existsMessageWithTimestamp:timestamp + sourceId:self.sourceId + sourceDeviceId:OWSDevicePrimaryDeviceId]; + XCTAssertFalse(result); + + // Different authorId + [[[TSIncomingMessage alloc] initWithTimestamp:timestamp + inThread:self.thread + authorId:@"some-other-author-id" + sourceDeviceId:OWSDevicePrimaryDeviceId + messageBody:@"foo"] save]; + + result = [self.finder existsMessageWithTimestamp:timestamp + sourceId:self.sourceId + sourceDeviceId:OWSDevicePrimaryDeviceId]; + XCTAssertFalse(result); + + // Different device + [[[TSIncomingMessage alloc] initWithTimestamp:timestamp + inThread:self.thread + authorId:self.sourceId + sourceDeviceId:OWSDevicePrimaryDeviceId + 1 + messageBody:@"foo"] save]; + + result = [self.finder existsMessageWithTimestamp:timestamp + sourceId:self.sourceId + sourceDeviceId:OWSDevicePrimaryDeviceId]; + XCTAssertFalse(result); + + // The real deal... + [[[TSIncomingMessage alloc] initWithTimestamp:timestamp + inThread:self.thread + authorId:self.sourceId + sourceDeviceId:OWSDevicePrimaryDeviceId + messageBody:@"foo"] save]; + + result = [self.finder existsMessageWithTimestamp:timestamp + sourceId:self.sourceId + sourceDeviceId:OWSDevicePrimaryDeviceId]; + XCTAssertTrue(result); +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/tests/Storage/OWSOrphanedDataCleanerTest.m b/tests/Storage/OWSOrphanedDataCleanerTest.m index e6be9cfb4..432d9e1ea 100644 --- a/tests/Storage/OWSOrphanedDataCleanerTest.m +++ b/tests/Storage/OWSOrphanedDataCleanerTest.m @@ -1,5 +1,8 @@ -// Copyright © 2016 Open Whisper Systems. All rights reserved. +// +// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// +#import "OWSDevice.h" #import "OWSOrphanedDataCleaner.h" #import "TSAttachmentStream.h" #import "TSContactThread.h" @@ -45,6 +48,7 @@ TSIncomingMessage *incomingMessage = [[TSIncomingMessage alloc] initWithTimestamp:1 inThread:unsavedThread authorId:@"fake-author-id" + sourceDeviceId:OWSDevicePrimaryDeviceId messageBody:@"footch"]; [incomingMessage save]; XCTAssertEqual(1, [TSIncomingMessage numberOfKeysInCollection]); @@ -61,6 +65,7 @@ TSIncomingMessage *incomingMessage = [[TSIncomingMessage alloc] initWithTimestamp:1 inThread:savedThread authorId:@"fake-author-id" + sourceDeviceId:OWSDevicePrimaryDeviceId messageBody:@"footch"]; [incomingMessage save]; XCTAssertEqual(1, [TSIncomingMessage numberOfKeysInCollection]); @@ -99,8 +104,10 @@ TSIncomingMessage *incomingMessage = [[TSIncomingMessage alloc] initWithTimestamp:1 inThread:savedThread authorId:@"fake-author-id" + sourceDeviceId:OWSDevicePrimaryDeviceId messageBody:@"footch" - attachmentIds:@[ attachmentStream.uniqueId ]]; + attachmentIds:@[ attachmentStream.uniqueId ] + expiresInSeconds:0]; [incomingMessage save]; NSString *attachmentFilePath = [attachmentStream filePath]; diff --git a/tests/Storage/TSMessageStorageTests.m b/tests/Storage/TSMessageStorageTests.m index 1850107b0..ac35145e3 100644 --- a/tests/Storage/TSMessageStorageTests.m +++ b/tests/Storage/TSMessageStorageTests.m @@ -1,9 +1,5 @@ // -// TSMessageStorageTests.m -// TextSecureKit -// -// Created by Frederic Jacobs on 16/11/14. -// Copyright (c) 2014 Open Whisper Systems. All rights reserved. +// Copyright (c) 2017 Open Whisper Systems. All rights reserved. // #import @@ -102,6 +98,7 @@ TSIncomingMessage *newMessage = [[TSIncomingMessage alloc] initWithTimestamp:timestamp inThread:self.thread authorId:[self.thread contactIdentifier] + sourceDeviceId:1 messageBody:body]; [[TSStorageManager sharedManager].newDatabaseConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { [newMessage saveWithTransaction:transaction]; @@ -126,6 +123,7 @@ TSIncomingMessage *newMessage = [[TSIncomingMessage alloc] initWithTimestamp:i inThread:self.thread authorId:[self.thread contactIdentifier] + sourceDeviceId:1 messageBody:body]; [messages addObject:newMessage]; [newMessage save]; @@ -174,6 +172,7 @@ TSIncomingMessage *newMessage = [[TSIncomingMessage alloc] initWithTimestamp:i inThread:thread authorId:@"Ed" + sourceDeviceId:1 messageBody:body]; [newMessage save]; [messages addObject:newMessage];