Dedupe incoming messags

// FREEBIE
This commit is contained in:
Michael Kirk 2017-02-15 18:32:27 -05:00
parent d6e1e81a8a
commit 975726e022
12 changed files with 260 additions and 12 deletions

View file

@ -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 <Mantle/MTLJSONAdapter.h>
@ -22,6 +24,11 @@ NS_ASSUME_NONNULL_BEGIN
*/
+ (void)replaceAll:(NSArray<OWSDevice *> *)devices;
/**
* The id of the device currently running this application
*/
+ (uint32_t)currentDeviceId;
/**
*
* @param transaction

View file

@ -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;

View file

@ -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<NSString *> *)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<NSString *> *)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;
/*

View file

@ -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<NSString *> *)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<NSString *> *)attachmentIds
expiresInSeconds:(uint32_t)expiresInSeconds
@ -60,6 +69,7 @@ NSString *const TSIncomingMessageWasReadOnThisDeviceNotification = @"TSIncomingM
}
_authorId = authorId;
_sourceDeviceId = sourceDeviceId;
_read = NO;
OWSAssert(self.receivedAtDate);

View file

@ -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];

View file

@ -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;

View file

@ -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

View file

@ -0,0 +1,156 @@
//
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
//
#import "OWSIncomingMessageFinder.h"
#import "TSIncomingMessage.h"
#import "TSStorageManager.h"
#import <YapDatabase/YapDatabase.h>
#import <YapDatabase/YapDatabaseSecondaryIndex.h>
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

View file

@ -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];

View file

@ -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];

View file

@ -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];

View file

@ -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 <XCTest/XCTest.h>
@ -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];