From 975726e022d89872b3c781769efb9c4e45dbb6d4 Mon Sep 17 00:00:00 2001 From: Michael Kirk Date: Wed, 15 Feb 2017 18:32:27 -0500 Subject: [PATCH] Dedupe incoming messags // FREEBIE --- src/Devices/OWSDevice.h | 9 +- src/Devices/OWSDevice.m | 13 +- src/Messages/Interactions/TSIncomingMessage.h | 10 ++ src/Messages/Interactions/TSIncomingMessage.m | 12 +- src/Messages/OWSMessageSender.m | 2 + src/Messages/TSMessagesManager.m | 18 +- src/Storage/OWSIncomingMessageFinder.h | 28 ++++ src/Storage/OWSIncomingMessageFinder.m | 156 ++++++++++++++++++ src/Storage/TSStorageManager.m | 2 + tests/Contacts/TSThreadTest.m | 6 +- tests/Storage/OWSOrphanedDataCleanerTest.m | 7 +- tests/Storage/TSMessageStorageTests.m | 9 +- 12 files changed, 260 insertions(+), 12 deletions(-) create mode 100644 src/Storage/OWSIncomingMessageFinder.h create mode 100644 src/Storage/OWSIncomingMessageFinder.m diff --git a/src/Devices/OWSDevice.h b/src/Devices/OWSDevice.h index 774faa521..54209673b 100644 --- a/src/Devices/OWSDevice.h +++ b/src/Devices/OWSDevice.h @@ -1,4 +1,6 @@ -// Copyright © 2016 Open Whisper Systems. All rights reserved. +// +// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// #import "TSYapDatabaseObject.h" #import @@ -22,6 +24,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..14842667e 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; +static 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..afa4e3319 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,6 +33,7 @@ extern NSString *const TSIncomingMessageWasReadOnThisDeviceNotification; - (instancetype)initWithTimestamp:(uint64_t)timestamp inThread:(TSThread *)thread authorId:(NSString *)authorId + sourceDeviceId:(uint32_t)sourceDeviceId messageBody:(nullable NSString *)body; /** @@ -42,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 @@ -52,6 +57,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; @@ -64,6 +70,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 +84,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; @@ -120,6 +129,7 @@ extern NSString *const TSIncomingMessageWasReadOnThisDeviceNotification; + (nullable instancetype)findMessageWithAuthorId:(NSString *)authorId timestamp:(uint64_t)timestamp; @property (nonatomic, readonly) NSString *authorId; +@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..d03df08fc 100644 --- a/src/Messages/Interactions/TSIncomingMessage.m +++ b/src/Messages/Interactions/TSIncomingMessage.m @@ -22,20 +22,28 @@ 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:@[]]; + return [self initWithTimestamp:timestamp + inThread:thread + authorId:authorId + sourceDeviceId:sourceDeviceId + messageBody:body + attachmentIds:@[]]; } - (instancetype)initWithTimestamp:(uint64_t)timestamp inThread:(TSThread *)thread authorId:(NSString *)authorId + sourceDeviceId:(uint32_t)sourceDeviceId messageBody:(nullable NSString *)body attachmentIds:(NSArray *)attachmentIds { return [self initWithTimestamp:timestamp inThread:thread authorId:authorId + sourceDeviceId:sourceDeviceId messageBody:body attachmentIds:attachmentIds expiresInSeconds:0]; @@ -44,6 +52,7 @@ NSString *const TSIncomingMessageWasReadOnThisDeviceNotification = @"TSIncomingM - (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 +69,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..00114498e 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,15 @@ NS_ASSUME_NONNULL_BEGIN - (void)handleEnvelope:(OWSSignalServiceProtosEnvelope *)envelope plaintextData:(NSData *)plaintextData { OWSAssert([NSThread isMainThread]); + + 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 +302,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 +623,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 +645,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]; @@ -658,12 +672,14 @@ NS_ASSUME_NONNULL_BEGIN textMessage = [[TSIncomingMessage alloc] initWithTimestamp:textMessageTimestamp inThread:gThread authorId:envelope.source + sourceDeviceId:envelope.sourceDevice messageBody:body]; } else { TSContactThread *cThread = (TSContactThread *)thread; textMessage = [[TSIncomingMessage alloc] initWithTimestamp:textMessageTimestamp inThread:cThread authorId:[cThread contactIdentifier] + sourceDeviceId:envelope.sourceDevice messageBody:body]; } textMessage.expiresInSeconds = dataMessage.expireTimer; 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..3c6161474 --- /dev/null +++ b/src/Storage/OWSIncomingMessageFinder.m @@ -0,0 +1,156 @@ +// +// 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; +@property (nonatomic, readonly) BOOL isExtensionRegistered; + +@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 registerExtension:self.indexExtension withName:OWSIncomingMessageFinderExtensionName]; +} + +- (void)registerExtension +{ + OWSAssert(NO); + 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..117e30ba6 100644 --- a/tests/Contacts/TSThreadTest.m +++ b/tests/Contacts/TSThreadTest.m @@ -1,4 +1,6 @@ -// Copyright © 2016 Open Whisper Systems. All rights reserved. +// +// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// #import "TSAttachmentStream.h" #import "TSContactThread.h" @@ -38,6 +40,7 @@ TSIncomingMessage *incomingMessage = [[TSIncomingMessage alloc] initWithTimestamp:10000 inThread:thread authorId:@"fake-author-id" + sourceDeviceId:1 messageBody:@"Incoming message body"]; [incomingMessage save]; @@ -74,6 +77,7 @@ [[TSIncomingMessage alloc] initWithTimestamp:10000 inThread:thread authorId:@"fake-author-id" + sourceDeviceId:1 messageBody:@"incoming message body" attachmentIds:[NSMutableArray arrayWithObject:incomingAttachment.uniqueId]]; [incomingMessage save]; diff --git a/tests/Storage/OWSOrphanedDataCleanerTest.m b/tests/Storage/OWSOrphanedDataCleanerTest.m index e6be9cfb4..85c674fcc 100644 --- a/tests/Storage/OWSOrphanedDataCleanerTest.m +++ b/tests/Storage/OWSOrphanedDataCleanerTest.m @@ -1,4 +1,6 @@ -// Copyright © 2016 Open Whisper Systems. All rights reserved. +// +// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// #import "OWSOrphanedDataCleaner.h" #import "TSAttachmentStream.h" @@ -45,6 +47,7 @@ TSIncomingMessage *incomingMessage = [[TSIncomingMessage alloc] initWithTimestamp:1 inThread:unsavedThread authorId:@"fake-author-id" + sourceDeviceId:1 messageBody:@"footch"]; [incomingMessage save]; XCTAssertEqual(1, [TSIncomingMessage numberOfKeysInCollection]); @@ -61,6 +64,7 @@ TSIncomingMessage *incomingMessage = [[TSIncomingMessage alloc] initWithTimestamp:1 inThread:savedThread authorId:@"fake-author-id" + sourceDeviceId:1 messageBody:@"footch"]; [incomingMessage save]; XCTAssertEqual(1, [TSIncomingMessage numberOfKeysInCollection]); @@ -99,6 +103,7 @@ TSIncomingMessage *incomingMessage = [[TSIncomingMessage alloc] initWithTimestamp:1 inThread:savedThread authorId:@"fake-author-id" + sourceDeviceId:1 messageBody:@"footch" attachmentIds:@[ attachmentStream.uniqueId ]]; [incomingMessage save]; 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];