From 98d1c59bfcc277936e2206a6b726e93cf448c6a0 Mon Sep 17 00:00:00 2001 From: Michael Kirk Date: Thu, 25 Aug 2016 19:01:35 -0400 Subject: [PATCH] Sync Contacts with Desktop * TODO refactor attachment sending to work without thread/message * TODO de-dupe attachment pointer building code // FREEBIE --- .../OWSOutgoingSentMessageTranscript.m | 15 +-- .../Interactions/OWSOutgoingSyncMessage.m | 18 +++ src/Messages/OWSSyncContactsMessage.h | 12 ++ src/Messages/OWSSyncContactsMessage.m | 124 ++++++++++++++++++ src/Messages/TSMessagesManager+attachments.m | 20 +-- src/Messages/TSMessagesManager+sendMessages.m | 15 ++- src/Messages/TSMessagesManager.m | 32 ++++- src/Protocols/ContactsManagerProtocol.h | 11 +- src/Util/MIMETypeUtil.h | 4 +- src/Util/MIMETypeUtil.m | 32 +++++ 10 files changed, 240 insertions(+), 43 deletions(-) create mode 100644 src/Messages/OWSSyncContactsMessage.h create mode 100644 src/Messages/OWSSyncContactsMessage.m diff --git a/src/Messages/Interactions/OWSOutgoingSentMessageTranscript.m b/src/Messages/Interactions/OWSOutgoingSentMessageTranscript.m index b82642236..d98a8f6b6 100644 --- a/src/Messages/Interactions/OWSOutgoingSentMessageTranscript.m +++ b/src/Messages/Interactions/OWSOutgoingSentMessageTranscript.m @@ -39,27 +39,18 @@ NS_ASSUME_NONNULL_BEGIN - (OWSSignalServiceProtosSyncMessage *)buildSyncMessage { + OWSSignalServiceProtosSyncMessageBuilder *syncMessageBuilder = [OWSSignalServiceProtosSyncMessageBuilder new]; + OWSSignalServiceProtosSyncMessageSentBuilder *sentBuilder = [OWSSignalServiceProtosSyncMessageSentBuilder new]; [sentBuilder setTimestamp:self.message.timestamp]; [sentBuilder setDestination:self.message.recipientIdentifier]; + [sentBuilder setMessage:[self.message buildDataMessage]]; - OWSSignalServiceProtosDataMessage *dataMessage = [self.message buildDataMessage]; - [sentBuilder setMessage:dataMessage]; - - OWSSignalServiceProtosSyncMessageBuilder *syncMessageBuilder = [OWSSignalServiceProtosSyncMessageBuilder new]; [syncMessageBuilder setSent:[sentBuilder build]]; return [syncMessageBuilder build]; } -- (NSData *)buildPlainTextData -{ - OWSSignalServiceProtosContentBuilder *contentBuilder = [OWSSignalServiceProtosContentBuilder new]; - [contentBuilder setSyncMessage:[self buildSyncMessage]]; - - return [[contentBuilder build] data]; -} - @end NS_ASSUME_NONNULL_END diff --git a/src/Messages/Interactions/OWSOutgoingSyncMessage.m b/src/Messages/Interactions/OWSOutgoingSyncMessage.m index e3cee10f1..4c2af2cb3 100644 --- a/src/Messages/Interactions/OWSOutgoingSyncMessage.m +++ b/src/Messages/Interactions/OWSOutgoingSyncMessage.m @@ -1,6 +1,7 @@ // Copyright © 2016 Open Whisper Systems. All rights reserved. #import "OWSOutgoingSyncMessage.h" +#import "OWSSignalServiceProtos.pb.h" NS_ASSUME_NONNULL_BEGIN @@ -11,6 +12,23 @@ NS_ASSUME_NONNULL_BEGIN return NO; } +- (OWSSignalServiceProtosSyncMessage *)buildSyncMessage +{ + NSAssert(NO, @"buildSyncMessage must be overridden in suclass"); + + OWSSignalServiceProtosSyncMessageBuilder *syncMessageBuilder = [OWSSignalServiceProtosSyncMessageBuilder new]; + return [syncMessageBuilder build]; +} + +- (NSData *)buildPlainTextData +{ + OWSSignalServiceProtosContentBuilder *contentBuilder = [OWSSignalServiceProtosContentBuilder new]; + [contentBuilder setSyncMessage:[self buildSyncMessage]]; + + return [[contentBuilder build] data]; +} + + @end NS_ASSUME_NONNULL_END diff --git a/src/Messages/OWSSyncContactsMessage.h b/src/Messages/OWSSyncContactsMessage.h new file mode 100644 index 000000000..dd515fb1e --- /dev/null +++ b/src/Messages/OWSSyncContactsMessage.h @@ -0,0 +1,12 @@ +// Copyright © 2016 Open Whisper Systems. All rights reserved. + +#import "ContactsManagerProtocol.h" +#import "OWSOutgoingSyncMessage.h" + +@interface OWSSyncContactsMessage : OWSOutgoingSyncMessage + +- (instancetype)initWithContactsManager:(id)contactsManager; +- (void)saveWithTransaction:(YapDatabaseReadWriteTransaction *)transaction; +- (NSData *)buildPlainTextAttachmentData; + +@end diff --git a/src/Messages/OWSSyncContactsMessage.m b/src/Messages/OWSSyncContactsMessage.m new file mode 100644 index 000000000..0400529a7 --- /dev/null +++ b/src/Messages/OWSSyncContactsMessage.m @@ -0,0 +1,124 @@ +// Copyright © 2016 Open Whisper Systems. All rights reserved. + +#import "OWSSyncContactsMessage.h" +#import "Contact.h" +#import "NSDate+millisecondTimeStamp.h" +#import "OWSSignalServiceProtos.pb.h" +#import "TSAttachment.h" +#import "TSAttachmentStream.h" +#import + +@interface OWSSyncContactsMessage () + +@property (nonatomic, readonly) id contactsManager; + +@end + +@implementation OWSSyncContactsMessage + +- (instancetype)initWithContactsManager:(id)contactsManager +{ + self = [super initWithTimestamp:[NSDate ows_millisecondTimeStamp] inThread:nil messageBody:nil attachmentIds:@[]]; + if (!self) { + return self; + } + + _contactsManager = contactsManager; + + return self; +} + +- (void)saveWithTransaction:(YapDatabaseReadWriteTransaction *)transaction +{ + // no-op + + // There's no need to save this message, since it's not displayed to the user. + // Furthermore if we did save it, we probably don't want to save the conctactsManager property. +} + +- (OWSSignalServiceProtosSyncMessage *)buildSyncMessage +{ + OWSSignalServiceProtosSyncMessageBuilder *syncMessageBuilder = [OWSSignalServiceProtosSyncMessageBuilder new]; + + if (self.attachmentIds.count != 1) { + DDLogError(@"expected sync contact message to have exactly one attachment, but found %lu", + (unsigned long)self.attachmentIds.count); + } + TSAttachment *attachment = [TSAttachmentStream fetchObjectWithUniqueID:self.attachmentIds[0]]; + + OWSSignalServiceProtosAttachmentPointerBuilder *attachmentBuilder = + [OWSSignalServiceProtosAttachmentPointerBuilder new]; + + [attachmentBuilder setId:[attachment.identifier unsignedLongLongValue]]; + [attachmentBuilder setContentType:attachment.contentType]; + [attachmentBuilder setKey:attachment.encryptionKey]; + + OWSSignalServiceProtosSyncMessageContactsBuilder *contactsBuilder = + [OWSSignalServiceProtosSyncMessageContactsBuilder new]; + [contactsBuilder setBlob:[attachmentBuilder build]]; + + [syncMessageBuilder setContacts:[contactsBuilder build]]; + + return [syncMessageBuilder build]; +} + +- (NSData *)buildPlainTextAttachmentData +{ + NSString *fileName = + [NSString stringWithFormat:@"%@_%@", [[NSProcessInfo processInfo] globallyUniqueString], @"contacts.dat"]; + NSURL *fileURL = [NSURL fileURLWithPath:[NSTemporaryDirectory() stringByAppendingPathComponent:fileName]]; + NSOutputStream *fileOutputStream = [NSOutputStream outputStreamWithURL:fileURL append:NO]; + [fileOutputStream open]; + + PBCodedOutputStream *outputStream = [PBCodedOutputStream streamWithOutputStream:fileOutputStream]; + DDLogInfo(@"Writing contacts data to %@", fileURL); + for (Contact *contact in self.contactsManager.signalContacts) { + OWSSignalServiceProtosContactDetailsBuilder *contactBuilder = [OWSSignalServiceProtosContactDetailsBuilder new]; + + [contactBuilder setName:contact.fullName]; + [contactBuilder setNumber:contact.textSecureIdentifiers.firstObject]; + + NSData *avatarPng; + if (contact.image) { + OWSSignalServiceProtosContactDetailsAvatarBuilder *avatarBuilder = + [OWSSignalServiceProtosContactDetailsAvatarBuilder new]; + + [avatarBuilder setContentType:@"image/png"]; + avatarPng = UIImagePNGRepresentation(contact.image); + // TODO check datasize and safely cast to int + [avatarBuilder setLength:(uint32_t)avatarPng.length]; + [contactBuilder setAvatar:[avatarBuilder build]]; + } + + NSData *contactData = [[contactBuilder build] data]; + + uint32_t contactDataLength = (uint32_t)contactData.length; + [outputStream writeRawVarint32:contactDataLength]; + [outputStream writeRawData:contactData]; + + if (contact.image) { + [outputStream writeRawData:avatarPng]; + } + } + [outputStream flush]; + [fileOutputStream close]; + + // TODO pass stream to builder rather than data as a singular hulk. + [NSInputStream inputStreamWithURL:fileURL]; + NSError *error; + NSData *data = [NSData dataWithContentsOfURL:fileURL options:NSDataReadingMappedIfSafe error:&error]; + if (error) { + DDLogError(@"Failed to read back contact data after writing it to %@ with error:%@", fileURL, error); + } + return data; + + // TODO delete contacts file. + // NSError *error; + // NSFileManager *manager = [NSFileManager defaultManager]; + // [manager removeItemAtURL:fileURL error:&error]; + // if (error) { + // DDLogError(@"Failed removing temp file at url:%@ with error:%@", fileURL, error); + // } +} + +@end diff --git a/src/Messages/TSMessagesManager+attachments.m b/src/Messages/TSMessagesManager+attachments.m index 23a17f516..7ef0871fc 100644 --- a/src/Messages/TSMessagesManager+attachments.m +++ b/src/Messages/TSMessagesManager+attachments.m @@ -85,25 +85,19 @@ dispatch_queue_t attachmentsQueue() { TSAttachmentEncryptionResult *result = [Cryptography encryptAttachment:attachmentData contentType:contentType identifier:attachmentId]; - [self.dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { - result.pointer.isDownloaded = NO; - [result.pointer saveWithTransaction:transaction]; - }]; + result.pointer.isDownloaded = NO; + [result.pointer save]; outgoingMessage.body = nil; [outgoingMessage.attachmentIds addObject:attachmentId]; if (outgoingMessage.groupMetaMessage != TSGroupMessageNew && outgoingMessage.groupMetaMessage != TSGroupMessageUpdate) { [outgoingMessage setMessageState:TSOutgoingMessageStateAttemptingOut]; - [self.dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { - [outgoingMessage saveWithTransaction:transaction]; - }]; + [outgoingMessage save]; } BOOL success = [self uploadDataWithProgress:result.body location:location attachmentID:attachmentId]; if (success) { - [self.dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { - result.pointer.isDownloaded = YES; - [result.pointer saveWithTransaction:transaction]; - }]; + result.pointer.isDownloaded = YES; + [result.pointer save]; [self sendMessage:outgoingMessage inThread:thread success:^{ @@ -248,7 +242,7 @@ dispatch_queue_t attachmentsQueue() { AFHTTPSessionManager *manager = [AFHTTPSessionManager manager]; manager.requestSerializer = [AFHTTPRequestSerializer serializer]; - [manager.requestSerializer setValue:@"application/octet-stream" forHTTPHeaderField:@"Content-Type"]; + [manager.requestSerializer setValue:OWSMimeTypeApplicationOctetStream forHTTPHeaderField:@"Content-Type"]; manager.responseSerializer = [AFHTTPResponseSerializer serializer]; manager.completionQueue = dispatch_get_main_queue(); @@ -285,7 +279,7 @@ dispatch_queue_t attachmentsQueue() { NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:[NSURL URLWithString:location]]; request.HTTPMethod = @"PUT"; request.HTTPBody = cipherText; - [request setValue:@"application/octet-stream" forHTTPHeaderField:@"Content-Type"]; + [request setValue:OWSMimeTypeApplicationOctetStream forHTTPHeaderField:@"Content-Type"]; AFURLSessionManager *manager = [[AFURLSessionManager alloc] initWithSessionConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]]; diff --git a/src/Messages/TSMessagesManager+sendMessages.m b/src/Messages/TSMessagesManager+sendMessages.m index ab7b70147..5d9abc310 100644 --- a/src/Messages/TSMessagesManager+sendMessages.m +++ b/src/Messages/TSMessagesManager+sendMessages.m @@ -122,16 +122,23 @@ dispatch_queue_t sendingQueue() { [self saveMessage:message withState:TSOutgoingMessageStateUnsent]; }]; - } else if ([thread isKindOfClass:[TSContactThread class]]) { + } 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]]) { + if (![contactThread.contactIdentifier isEqualToString:[TSAccountManager localNumber]] || + [message isKindOfClass:[OWSOutgoingSyncMessage class]]) { + + NSString *recipientContactId = [message isKindOfClass:[OWSOutgoingSyncMessage class]] + ? [TSAccountManager localNumber] + : contactThread.contactIdentifier; + __block SignalRecipient *recipient; [self.dbConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) { - recipient = [SignalRecipient recipientWithTextSecureIdentifier:contactThread.contactIdentifier - withTransaction:transaction]; + recipient = [SignalRecipient recipientWithTextSecureIdentifier:recipientContactId + withTransaction:transaction]; }]; if (!recipient) { diff --git a/src/Messages/TSMessagesManager.m b/src/Messages/TSMessagesManager.m index 15f982c7c..cc6e08e17 100644 --- a/src/Messages/TSMessagesManager.m +++ b/src/Messages/TSMessagesManager.m @@ -2,8 +2,11 @@ // Copyright (c) 2014 Open Whisper Systems. All rights reserved. #import "TSMessagesManager.h" +#import "ContactsManagerProtocol.h" +#import "MimeTypeUtil.h" #import "NSData+messagePadding.h" #import "OWSIncomingSentMessageTranscript.h" +#import "OWSSyncContactsMessage.h" #import "TSAccountManager.h" #import "TSAttachmentStream.h" #import "TSCall.h" @@ -19,10 +22,6 @@ #import #import -@interface TSMessagesManager () - -@end - @implementation TSMessagesManager + (instancetype)sharedManager { @@ -231,10 +230,33 @@ withSyncMessage:(OWSSignalServiceProtosSyncMessage *)syncMessage { if (syncMessage.hasSent) { - DDLogInfo(@"Received sent message transcription"); + DDLogInfo(@"Received `sent` syncMessage, recording message transcript."); OWSIncomingSentMessageTranscript *transcript = [[OWSIncomingSentMessageTranscript alloc] initWithProto:syncMessage.sent relay:messageEnvelope.relay]; [transcript record]; + } + if (syncMessage.hasRequest) { + if (syncMessage.request.type == OWSSignalServiceProtosSyncMessageRequestTypeContacts) { + DDLogInfo(@"Received Contacts `request` syncMessage."); + + OWSSyncContactsMessage *syncContactsMessage = + [[OWSSyncContactsMessage alloc] initWithContactsManager:[TextSecureKitEnv sharedEnv].contactsManager]; + + [self sendAttachment:[syncContactsMessage buildPlainTextAttachmentData] + contentType:OWSMimeTypeApplicationOctetStream + inMessage:syncContactsMessage + thread:nil + success:^{ + DDLogInfo(@"Successfully sent Contacts response syncMessage."); + } + failure:^{ + DDLogError(@"Failed to send Contacts response syncMessage."); + }]; + + } else if (syncMessage.request.type == OWSSignalServiceProtosSyncMessageRequestTypeGroups) { + DDLogInfo(@"Received Contacts `groups` syncMessage."); + // TODO + } } else { DDLogWarn(@"Ignoring unsupported sync message."); } diff --git a/src/Protocols/ContactsManagerProtocol.h b/src/Protocols/ContactsManagerProtocol.h index cf2061518..318374d14 100644 --- a/src/Protocols/ContactsManagerProtocol.h +++ b/src/Protocols/ContactsManagerProtocol.h @@ -1,18 +1,13 @@ -// -// ContactsManagerProtocol.h -// Pods -// // Created by Frederic Jacobs on 05/12/15. -// -// - -#import +// Copyright © 2016 Open Whisper Systems. All rights reserved. @class PhoneNumber; +@class Contact; @protocol ContactsManagerProtocol - (NSString *)nameStringForPhoneIdentifier:(NSString *)phoneNumber; +- (NSArray *)signalContacts; + (BOOL)name:(NSString *)nameString matchesQuery:(NSString *)queryString; #if TARGET_OS_IPHONE diff --git a/src/Util/MIMETypeUtil.h b/src/Util/MIMETypeUtil.h index 85940ca7d..2f57a582f 100644 --- a/src/Util/MIMETypeUtil.h +++ b/src/Util/MIMETypeUtil.h @@ -1,4 +1,6 @@ -#import +// Copyright © 2016 Open Whisper Systems. All rights reserved. + +extern NSString *const OWSMimeTypeApplicationOctetStream; @interface MIMETypeUtil : NSObject diff --git a/src/Util/MIMETypeUtil.m b/src/Util/MIMETypeUtil.m index b5b00972a..10d9f4f88 100644 --- a/src/Util/MIMETypeUtil.m +++ b/src/Util/MIMETypeUtil.m @@ -3,6 +3,7 @@ #import "UIImage+contentTypes.h" #endif +NSString *const OWSMimeTypeApplicationOctetStream = @"application/octet-stream"; @implementation MIMETypeUtil @@ -56,6 +57,13 @@ }; } ++ (NSDictionary *)supportedBinaryDataMIMETypesToExtensionTypes +{ + return @{ + OWSMimeTypeApplicationOctetStream : @"dat", + }; +} + + (NSDictionary *)supportedVideoExtensionTypesToMIMETypes { return @{ @"3gp" : @"video/3gpp", @@ -130,6 +138,11 @@ return [[self supportedAnimatedMIMETypesToExtensionTypes] objectForKey:contentType] != nil; } ++ (BOOL)isSupportedBinaryDataMIMEType:(NSString *)contentType +{ + return [[self supportedBinaryDataMIMETypesToExtensionTypes] objectForKey:contentType] != nil; +} + + (BOOL)isSupportedMIMEType:(NSString *)contentType { return [self isSupportedImageMIMEType:contentType] || [self isSupportedAudioMIMEType:contentType] || [self isSupportedVideoMIMEType:contentType] || [self isSupportedAnimatedMIMEType:contentType]; @@ -167,6 +180,11 @@ return [[self supportedAnimatedMIMETypesToExtensionTypes] objectForKey:supportedMIMEType]; } ++ (NSString *)getSupportedExtensionFromBinaryDataMIMEType:(NSString *)supportedMIMEType +{ + return [[self supportedBinaryDataMIMETypesToExtensionTypes] objectForKey:supportedMIMEType]; +} + + (NSString *)getSupportedMIMETypeFromVideoFile:(NSString *)supportedVideoFile { return [[self supportedVideoExtensionTypesToMIMETypes] objectForKey:[supportedVideoFile pathExtension]]; } @@ -187,6 +205,12 @@ + (BOOL)isAnimated:(NSString *)contentType { return [MIMETypeUtil isSupportedAnimatedMIMEType:contentType]; } + ++ (BOOL)isBinaryData:(NSString *)contentType +{ + return [MIMETypeUtil isSupportedBinaryDataMIMEType:contentType]; +} + + (BOOL)isImage:(NSString *)contentType { return [MIMETypeUtil isSupportedImageMIMEType:contentType]; } @@ -210,6 +234,8 @@ return [MIMETypeUtil filePathForImage:uniqueId ofMIMEType:contentType inFolder:folder]; } else if ([self isAnimated:contentType]) { return [MIMETypeUtil filePathForAnimated:uniqueId ofMIMEType:contentType inFolder:folder]; + } else if ([self isBinaryData:contentType]) { + return [MIMETypeUtil filePathForBinaryData:uniqueId ofMIMEType:contentType inFolder:folder]; } DDLogError(@"Got asked for path of file %@ which is unsupported", contentType); @@ -257,6 +283,12 @@ stringByAppendingPathExtension:[self getSupportedExtensionFromAnimatedMIMEType:contentType]]; } ++ (NSString *)filePathForBinaryData:(NSString *)uniqueId ofMIMEType:(NSString *)contentType inFolder:(NSString *)folder +{ + return [[folder stringByAppendingFormat:@"/%@", uniqueId] + stringByAppendingPathExtension:[self getSupportedExtensionFromBinaryDataMIMEType:contentType]]; +} + #if TARGET_OS_IPHONE + (NSString *)getSupportedImageMIMETypeFromImage:(UIImage *)image {