diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m index 7b45cbdc7..bad4ced52 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m +++ b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m @@ -3600,7 +3600,8 @@ typedef enum : NSUInteger { TSOutgoingMessage *message = [ThreadUtil enqueueMessageWithAttachments:attachments messageBody:messageText inThread:self.thread - quotedReplyModel:self.inputToolbar.quotedReply]; + quotedReplyModel:self.inputToolbar.quotedReply + linkPreview:nil]; [self messageWasSent:message]; @@ -3977,7 +3978,8 @@ typedef enum : NSUInteger { // before the attachment is downloaded) message = [ThreadUtil enqueueMessageWithAttachment:attachment inThread:self.thread - quotedReplyModel:self.inputToolbar.quotedReply]; + quotedReplyModel:self.inputToolbar.quotedReply + linkPreview:nil]; } else { [self.uiDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction *_Nonnull transaction) { message = [ThreadUtil enqueueMessageWithText:text diff --git a/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m b/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m index b0a086305..6ddb77042 100644 --- a/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m +++ b/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m @@ -425,7 +425,7 @@ NS_ASSUME_NONNULL_BEGIN [DDLog flushLog]; } OWSAssertDebug(![attachment hasError]); - [ThreadUtil enqueueMessageWithAttachment:attachment inThread:thread quotedReplyModel:nil]; + [ThreadUtil enqueueMessageWithAttachment:attachment inThread:thread quotedReplyModel:nil linkPreview:nil]; success(); } @@ -1741,7 +1741,7 @@ NS_ASSUME_NONNULL_BEGIN OWSAssertDebug(thread); SignalAttachment *attachment = [self signalAttachmentForFilePath:filePath]; - [ThreadUtil enqueueMessageWithAttachment:attachment inThread:thread quotedReplyModel:nil]; + [ThreadUtil enqueueMessageWithAttachment:attachment inThread:thread quotedReplyModel:nil linkPreview:nil]; success(); } @@ -3346,7 +3346,7 @@ typedef OWSContact * (^OWSContactBlock)(YapDatabaseReadWriteTransaction *transac DataSource *_Nullable dataSource = [DataSourceValue dataSourceWithOversizeText:message]; SignalAttachment *attachment = [SignalAttachment attachmentWithDataSource:dataSource dataUTI:kOversizeTextAttachmentUTI]; - [ThreadUtil enqueueMessageWithAttachment:attachment inThread:thread quotedReplyModel:nil]; + [ThreadUtil enqueueMessageWithAttachment:attachment inThread:thread quotedReplyModel:nil linkPreview:nil]; } + (NSData *)createRandomNSDataOfSize:(size_t)size @@ -3379,7 +3379,7 @@ typedef OWSContact * (^OWSContactBlock)(YapDatabaseReadWriteTransaction *transac // style them indistinguishably from a separate text message. attachment.captionText = [self randomCaptionText]; } - [ThreadUtil enqueueMessageWithAttachment:attachment inThread:thread quotedReplyModel:nil]; + [ThreadUtil enqueueMessageWithAttachment:attachment inThread:thread quotedReplyModel:nil linkPreview:nil]; } + (SSKProtoEnvelope *)createEnvelopeForThread:(TSThread *)thread @@ -4445,7 +4445,7 @@ typedef OWSContact * (^OWSContactBlock)(YapDatabaseReadWriteTransaction *transac [DDLog flushLog]; } OWSAssertDebug(![attachment hasError]); - [ThreadUtil enqueueMessageWithAttachment:attachment inThread:thread quotedReplyModel:nil]; + [ThreadUtil enqueueMessageWithAttachment:attachment inThread:thread quotedReplyModel:nil linkPreview:nil]; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{ sendUnsafeFile(); @@ -4763,7 +4763,8 @@ typedef OWSContact * (^OWSContactBlock)(YapDatabaseReadWriteTransaction *transac TSOutgoingMessage *message = [ThreadUtil enqueueMessageWithAttachments:attachments messageBody:messageBody inThread:thread - quotedReplyModel:nil]; + quotedReplyModel:nil + linkPreview:nil]; OWSLogError(@"timestamp: %llu.", message.timestamp); }]; } diff --git a/Signal/src/ViewControllers/DebugUI/DebugUIMisc.m b/Signal/src/ViewControllers/DebugUI/DebugUIMisc.m index 16bc970bb..2e5cd0657 100644 --- a/Signal/src/ViewControllers/DebugUI/DebugUIMisc.m +++ b/Signal/src/ViewControllers/DebugUI/DebugUIMisc.m @@ -1,8 +1,9 @@ // -// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. // #import "DebugUIMisc.h" +#import "DebugUIMessagesAssetLoader.h" #import "OWSBackup.h" #import "OWSCountryMetadata.h" #import "OWSTableViewController.h" @@ -20,7 +21,6 @@ #import #import #import -#import "DebugUIMessagesAssetLoader.h" NS_ASSUME_NONNULL_BEGIN @@ -257,7 +257,7 @@ NS_ASSUME_NONNULL_BEGIN OWSFailDebug(@"attachment[%@]: %@", [attachment sourceFilename], [attachment errorName]); return; } - [ThreadUtil enqueueMessageWithAttachment:attachment inThread:thread quotedReplyModel:nil]; + [ThreadUtil enqueueMessageWithAttachment:attachment inThread:thread quotedReplyModel:nil linkPreview:nil]; } + (void)sendUnencryptedDatabase:(TSThread *)thread @@ -279,7 +279,7 @@ NS_ASSUME_NONNULL_BEGIN OWSFailDebug(@"attachment[%@]: %@", [attachment sourceFilename], [attachment errorName]); return; } - [ThreadUtil enqueueMessageWithAttachment:attachment inThread:thread quotedReplyModel:nil]; + [ThreadUtil enqueueMessageWithAttachment:attachment inThread:thread quotedReplyModel:nil linkPreview:nil]; } #ifdef DEBUG diff --git a/SignalMessaging/utils/ThreadUtil.h b/SignalMessaging/utils/ThreadUtil.h index 6a20274ee..64a0052e6 100644 --- a/SignalMessaging/utils/ThreadUtil.h +++ b/SignalMessaging/utils/ThreadUtil.h @@ -53,12 +53,14 @@ NS_ASSUME_NONNULL_BEGIN + (TSOutgoingMessage *)enqueueMessageWithAttachment:(SignalAttachment *)attachment inThread:(TSThread *)thread - quotedReplyModel:(nullable OWSQuotedReplyModel *)quotedReplyModel; + quotedReplyModel:(nullable OWSQuotedReplyModel *)quotedReplyModel + linkPreview:(nullable OWSLinkPreview *)linkPreview; + (TSOutgoingMessage *)enqueueMessageWithAttachments:(NSArray *)attachments messageBody:(nullable NSString *)messageBody inThread:(TSThread *)thread - quotedReplyModel:(nullable OWSQuotedReplyModel *)quotedReplyModel; + quotedReplyModel:(nullable OWSQuotedReplyModel *)quotedReplyModel + linkPreview:(nullable OWSLinkPreview *)linkPreview; + (TSOutgoingMessage *)enqueueMessageWithContactShare:(OWSContact *)contactShare inThread:(TSThread *)thread; + (void)enqueueLeaveGroupMessageInThread:(TSGroupThread *)thread; diff --git a/SignalMessaging/utils/ThreadUtil.m b/SignalMessaging/utils/ThreadUtil.m index 0eb027186..16d6bd493 100644 --- a/SignalMessaging/utils/ThreadUtil.m +++ b/SignalMessaging/utils/ThreadUtil.m @@ -99,19 +99,22 @@ NS_ASSUME_NONNULL_BEGIN + (TSOutgoingMessage *)enqueueMessageWithAttachment:(SignalAttachment *)attachment inThread:(TSThread *)thread quotedReplyModel:(nullable OWSQuotedReplyModel *)quotedReplyModel + linkPreview:(nullable OWSLinkPreview *)linkPreview { return [self enqueueMessageWithAttachments:@[ attachment, ] messageBody:attachment.captionText inThread:thread - quotedReplyModel:quotedReplyModel]; + quotedReplyModel:quotedReplyModel + linkPreview:linkPreview]; } + (TSOutgoingMessage *)enqueueMessageWithAttachments:(NSArray *)attachments messageBody:(nullable NSString *)messageBody inThread:(TSThread *)thread quotedReplyModel:(nullable OWSQuotedReplyModel *)quotedReplyModel + linkPreview:(nullable OWSLinkPreview *)linkPreview { OWSAssertIsOnMainThread(); OWSAssertDebug(attachments.count > 0); @@ -137,7 +140,7 @@ NS_ASSUME_NONNULL_BEGIN groupMetaMessage:TSGroupMetaMessageUnspecified quotedMessage:[quotedReplyModel buildQuotedMessageForSending] contactShare:nil - linkPreview:nil]; + linkPreview:linkPreview]; NSMutableArray *attachmentInfos = [NSMutableArray new]; for (SignalAttachment *attachment in attachments) { diff --git a/SignalServiceKit/protobuf/SignalService.proto b/SignalServiceKit/protobuf/SignalService.proto index 4e3b7ccf4..2184337b9 100644 --- a/SignalServiceKit/protobuf/SignalService.proto +++ b/SignalServiceKit/protobuf/SignalService.proto @@ -222,7 +222,7 @@ message DataMessage { optional uint64 timestamp = 7; optional Quote quote = 8; repeated Contact contact = 9; - optional Preview preview = 10; + repeated Preview preview = 10; } message NullMessage { diff --git a/SignalServiceKit/src/Messages/Interactions/OWSLinkPreview.swift b/SignalServiceKit/src/Messages/Interactions/OWSLinkPreview.swift index 3dcb3ff34..1256c230d 100644 --- a/SignalServiceKit/src/Messages/Interactions/OWSLinkPreview.swift +++ b/SignalServiceKit/src/Messages/Interactions/OWSLinkPreview.swift @@ -10,10 +10,10 @@ public enum LinkPreviewError: Int, Error { case noPreview } -// MARK: - OWSLinkPreviewInfo +// MARK: - OWSLinkPreviewDraft // This contains the info for a link preview "draft". -public class OWSLinkPreviewInfo: NSObject { +public class OWSLinkPreviewDraft: NSObject { @objc public var urlString: String @@ -39,6 +39,11 @@ public class OWSLinkPreviewInfo: NSObject { let hasImage = imageFilePath != nil return hasTitle || hasImage } + + @objc + public func displayDomain() -> String? { + return OWSLinkPreview.displayDomain(forUrl: urlString) + } } // MARK: - OWSLinkPreview @@ -96,7 +101,7 @@ public class OWSLinkPreview: MTLModel { guard OWSLinkPreview.featureEnabled else { throw LinkPreviewError.noPreview } - guard let previewProto = dataMessage.preview else { + guard let previewProto = dataMessage.preview.first else { throw LinkPreviewError.noPreview } let urlString = previewProto.url @@ -122,7 +127,13 @@ public class OWSLinkPreview: MTLModel { throw LinkPreviewError.invalidInput } - let title: String? = previewProto.title?.trimmingCharacters(in: .whitespacesAndNewlines) + var title: String? + if let rawTitle = previewProto.title?.trimmingCharacters(in: .whitespacesAndNewlines) { + let normalizedTitle = OWSLinkPreview.normalizeTitle(title: rawTitle) + if normalizedTitle.count > 0 { + title = normalizedTitle + } + } var imageAttachmentId: String? if let imageProto = previewProto.image { @@ -146,7 +157,7 @@ public class OWSLinkPreview: MTLModel { } @objc - public class func buildValidatedLinkPreview(fromInfo info: OWSLinkPreviewInfo, + public class func buildValidatedLinkPreview(fromInfo info: OWSLinkPreviewDraft, transaction: YapDatabaseReadWriteTransaction) throws -> OWSLinkPreview { guard OWSLinkPreview.featureEnabled else { throw LinkPreviewError.noPreview @@ -187,8 +198,17 @@ public class OWSLinkPreview: MTLModel { owsFailDebug("Invalid content type for path: \(filePath)") return nil } + guard let dataSource = DataSourcePath.dataSource(withFilePath: filePath, shouldDeleteOnDeallocation: true) else { + owsFailDebug("Could not create data source for path: \(filePath)") + return nil + } let attachment = TSAttachmentStream(contentType: contentType, byteCount: fileSize.uint32Value, sourceFilename: nil, caption: nil, albumMessageId: nil) + guard attachment.write(dataSource) else { + owsFailDebug("Could not write data source for path: \(filePath)") + return nil + } attachment.save(with: transaction) + return attachment.uniqueId } @@ -214,6 +234,23 @@ public class OWSLinkPreview: MTLModel { attachment.remove(with: transaction) } + private class func normalizeTitle(title: String) -> String { + var result = title + // Truncate title after 2 lines of text. + let maxLineCount = 2 + var components = result.components(separatedBy: .newlines) + if components.count > maxLineCount { + components = Array(components[0.. maxCharacterCount { + let endIndex = result.index(result.startIndex, offsetBy: maxCharacterCount) + result = String(result[...endIndex]) + } + return result + } + // MARK: - Domain Whitelist // TODO: Finalize @@ -235,13 +272,36 @@ public class OWSLinkPreview: MTLModel { "https" ] + @objc + public func displayDomain() -> String? { + return OWSLinkPreview.displayDomain(forUrl: urlString) + } + + @objc + public class func displayDomain(forUrl urlString: String?) -> String? { + guard let urlString = urlString else { + owsFailDebug("Missing url.") + return nil + } + guard let url = URL(string: urlString) else { + owsFailDebug("Invalid url.") + return nil + } + guard let result = whitelistedDomain(forUrl: url, + domainWhitelist: OWSLinkPreview.linkDomainWhitelist) else { + owsFailDebug("Missing domain.") + return nil + } + return result + } + @objc public class func isValidLinkUrl(_ urlString: String) -> Bool { guard let url = URL(string: urlString) else { return false } - return isUrlInDomainWhitelist(url: url, - domainWhitelist: OWSLinkPreview.linkDomainWhitelist) + return whitelistedDomain(forUrl: url, + domainWhitelist: OWSLinkPreview.linkDomainWhitelist) != nil } @objc @@ -249,19 +309,19 @@ public class OWSLinkPreview: MTLModel { guard let url = URL(string: urlString) else { return false } - return isUrlInDomainWhitelist(url: url, - domainWhitelist: OWSLinkPreview.linkDomainWhitelist + OWSLinkPreview.mediaDomainWhitelist) + return whitelistedDomain(forUrl: url, + domainWhitelist: OWSLinkPreview.linkDomainWhitelist + OWSLinkPreview.mediaDomainWhitelist) != nil } - private class func isUrlInDomainWhitelist(url: URL, domainWhitelist: [String]) -> Bool { + private class func whitelistedDomain(forUrl url: URL, domainWhitelist: [String]) -> String? { guard let urlProtocol = url.scheme?.lowercased() else { - return false + return nil } guard protocolWhitelist.contains(urlProtocol) else { - return false + return nil } guard let domain = url.host?.lowercased() else { - return false + return nil } // TODO: We need to verify: // @@ -273,10 +333,10 @@ public class OWSLinkPreview: MTLModel { for whitelistedDomain in domainWhitelist { if domain == whitelistedDomain.lowercased() || domain.hasSuffix("." + whitelistedDomain.lowercased()) { - return true + return whitelistedDomain } } - return false + return nil } // MARK: - Serial Queue @@ -291,16 +351,16 @@ public class OWSLinkPreview: MTLModel { // MARK: - Text Parsing - // This cache should only be accessed on serialQueue. + // This cache should only be accessed on main thread. private static var previewUrlCache: NSCache = NSCache() - private class func previewUrl(forMessageBodyText body: String?) -> String? { - assertIsOnSerialQueue() + @objc + public class func previewUrl(forMessageBodyText body: String?) -> String? { + AssertIsOnMainThread() guard OWSLinkPreview.featureEnabled else { return nil } - guard let body = body else { return nil } @@ -324,33 +384,35 @@ public class OWSLinkPreview: MTLModel { // MARK: - Preview Construction // This cache should only be accessed on serialQueue. - private static var linkPreviewInfoCache: NSCache = NSCache() + private static var linkPreviewDraftCache: NSCache = NSCache() // Completion will always be invoked exactly once. // // The completion is called with a link preview if one can be built for // the message body. It building the preview fails, completion will be // called with nil to avoid failing the message send. - // - // NOTE: Completion might be invoked on any thread. @objc - public class func tryToBuildPreviewInfo(forMessageBodyText body: String?, - completion: @escaping (OWSLinkPreviewInfo?) -> Void) { + public class func tryToBuildPreviewInfo(previewUrl: String?, + callbackQueue: DispatchQueue, + completion completionParam: @escaping (OWSLinkPreviewDraft?) -> Void) { + + // Ensure we invoke completion on the callback queue. + let completion = { (linkPreviewDraft) in + callbackQueue.async { + completionParam(linkPreviewDraft) + } + } + guard OWSLinkPreview.featureEnabled else { completion(nil) return } - guard let body = body else { + guard let previewUrl = previewUrl else { completion(nil) return } serialQueue.async { - guard let previewUrl = previewUrl(forMessageBodyText: body) else { - completion(nil) - return - } - - if let cachedInfo = linkPreviewInfoCache.object(forKey: previewUrl as AnyObject) { + if let cachedInfo = linkPreviewDraftCache.object(forKey: previewUrl as AnyObject) { Logger.verbose("Link preview info cache hit.") completion(cachedInfo) return @@ -361,21 +423,19 @@ public class OWSLinkPreview: MTLModel { completion(nil) return } - parse(linkData: data, linkUrlString: previewUrl) { (linkPreviewInfo) in - guard let linkPreviewInfo = linkPreviewInfo else { + parse(linkData: data, linkUrlString: previewUrl) { (linkPreviewDraft) in + guard let linkPreviewDraft = linkPreviewDraft else { completion(nil) return } - guard linkPreviewInfo.isValid() else { + guard linkPreviewDraft.isValid() else { completion(nil) return } serialQueue.async { - previewUrlCache.setObject(linkPreviewInfo, forKey: previewUrl as AnyObject) + previewUrlCache.setObject(linkPreviewDraft, forKey: previewUrl as AnyObject) - DispatchQueue.global().async { - completion(linkPreviewInfo) - } + completion(linkPreviewDraft) } } } @@ -448,30 +508,36 @@ public class OWSLinkPreview: MTLModel { // private class func parse(linkData: Data, linkUrlString: String, - completion: @escaping (OWSLinkPreviewInfo?) -> Void) { + completion: @escaping (OWSLinkPreviewDraft?) -> Void) { guard let linkText = String(bytes: linkData, encoding: .utf8) else { owsFailDebug("Could not parse link text.") completion(nil) return } - Logger.verbose("linkText: \(linkText)") - let title = NSRegularExpression.parseFirstMatch(pattern: "", text: linkText) + var title: String? + if let rawTitle = NSRegularExpression.parseFirstMatch(pattern: "", text: linkText) { + let normalizedTitle = OWSLinkPreview.normalizeTitle(title: rawTitle) + if normalizedTitle.count > 0 { + title = normalizedTitle + } + } + Logger.verbose("title: \(String(describing: title))") guard let imageUrlString = NSRegularExpression.parseFirstMatch(pattern: "", text: linkText) else { - return completion(OWSLinkPreviewInfo(urlString: linkUrlString, title: title)) + return completion(OWSLinkPreviewDraft(urlString: linkUrlString, title: title)) } Logger.verbose("imageUrlString: \(imageUrlString)") guard let imageUrl = URL(string: imageUrlString) else { Logger.error("Could not parse image URL.") - return completion(OWSLinkPreviewInfo(urlString: linkUrlString, title: title)) + return completion(OWSLinkPreviewDraft(urlString: linkUrlString, title: title)) } let imageFilename = imageUrl.lastPathComponent let imageFileExtension = (imageFilename as NSString).pathExtension.lowercased() guard let imageMimeType = MIMETypeUtil.mimeType(forFileExtension: imageFileExtension) else { Logger.error("Image URL has unknown content type: \(imageFileExtension).") - return completion(OWSLinkPreviewInfo(urlString: linkUrlString, title: title)) + return completion(OWSLinkPreviewDraft(urlString: linkUrlString, title: title)) } let kValidMimeTypes = [ OWSMimeTypeImagePng, @@ -479,21 +545,21 @@ public class OWSLinkPreview: MTLModel { ] guard kValidMimeTypes.contains(imageMimeType) else { Logger.error("Image URL has invalid content type: \(imageMimeType).") - return completion(OWSLinkPreviewInfo(urlString: linkUrlString, title: title)) + return completion(OWSLinkPreviewDraft(urlString: linkUrlString, title: title)) } downloadContents(ofUrl: imageUrlString, completion: { (imageData) in guard let imageData = imageData else { Logger.error("Could not download image.") - return completion(OWSLinkPreviewInfo(urlString: linkUrlString, title: title)) + return completion(OWSLinkPreviewDraft(urlString: linkUrlString, title: title)) } let imageFilePath = OWSFileSystem.temporaryFilePath(withFileExtension: imageFileExtension) do { try imageData.write(to: NSURL.fileURL(withPath: imageFilePath), options: .atomicWrite) } catch let error as NSError { owsFailDebug("file write failed: \(imageFilePath), \(error)") - return completion(OWSLinkPreviewInfo(urlString: linkUrlString, title: title)) + return completion(OWSLinkPreviewDraft(urlString: linkUrlString, title: title)) } // NOTE: imageSize(forFilePath:...) will call ows_isValidImage(...). let imageSize = NSData.imageSize(forFilePath: imageFilePath, mimeType: imageMimeType) @@ -503,11 +569,11 @@ public class OWSLinkPreview: MTLModel { imageSize.width < kMaxImageSize, imageSize.height < kMaxImageSize else { Logger.error("Image has invalid size: \(imageSize).") - return completion(OWSLinkPreviewInfo(urlString: linkUrlString, title: title)) + return completion(OWSLinkPreviewDraft(urlString: linkUrlString, title: title)) } - let linkPreviewInfo = OWSLinkPreviewInfo(urlString: linkUrlString, title: title, imageFilePath: imageFilePath) - completion(linkPreviewInfo) + let linkPreviewDraft = OWSLinkPreviewDraft(urlString: linkUrlString, title: title, imageFilePath: imageFilePath) + completion(linkPreviewDraft) }) } } diff --git a/SignalServiceKit/src/Messages/Interactions/TSOutgoingMessage.m b/SignalServiceKit/src/Messages/Interactions/TSOutgoingMessage.m index 81e90e1ba..82505106f 100644 --- a/SignalServiceKit/src/Messages/Interactions/TSOutgoingMessage.m +++ b/SignalServiceKit/src/Messages/Interactions/TSOutgoingMessage.m @@ -989,7 +989,7 @@ NSString *NSStringForOutgoingMessageRecipientState(OWSOutgoingMessageRecipientSt if (error || !previewProto) { OWSFailDebug(@"Could not build link preview protobuf: %@.", error); } else { - [builder setPreview:previewProto]; + [builder addPreview:previewProto]; } } diff --git a/SignalServiceKit/src/Messages/OWSMessageManager.m b/SignalServiceKit/src/Messages/OWSMessageManager.m index 85ed660b3..5635bff66 100644 --- a/SignalServiceKit/src/Messages/OWSMessageManager.m +++ b/SignalServiceKit/src/Messages/OWSMessageManager.m @@ -1437,55 +1437,55 @@ NS_ASSUME_NONNULL_BEGIN [incomingMessage markAsReadAtTimestamp:envelope.timestamp sendReadReceipt:NO transaction:transaction]; } - TSQuotedMessage *_Nullable quotedMessage = incomingMessage.quotedMessage; - if (quotedMessage && quotedMessage.thumbnailAttachmentPointerId) { - // We weren't able to derive a local thumbnail, so we'll fetch the referenced attachment. - TSAttachmentPointer *attachmentPointer = - [TSAttachmentPointer fetchObjectWithUniqueID:quotedMessage.thumbnailAttachmentPointerId - transaction:transaction]; - - if ([attachmentPointer isKindOfClass:[TSAttachmentPointer class]]) { - OWSLogDebug(@"downloading thumbnail for message: %lu", (unsigned long)incomingMessage.timestamp); - [self.attachmentDownloads downloadAttachmentPointer:attachmentPointer - success:^(NSArray *attachmentStreams) { - OWSAssertDebug(attachmentStreams.count == 1); - TSAttachmentStream *attachmentStream = attachmentStreams.firstObject; - [self.dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { - [incomingMessage setQuotedMessageThumbnailAttachmentStream:attachmentStream]; - [incomingMessage saveWithTransaction:transaction]; - }]; - } - failure:^(NSError *error) { - OWSLogWarn(@"failed to fetch thumbnail for message: %lu with error: %@", - (unsigned long)incomingMessage.timestamp, - error); - }]; - } + NSMutableArray *otherAttachmentIds = [NSMutableArray new]; + if (incomingMessage.quotedMessage.thumbnailAttachmentPointerId.length > 0) { + [otherAttachmentIds addObject:incomingMessage.quotedMessage.thumbnailAttachmentPointerId]; } - - OWSContact *_Nullable contact = incomingMessage.contactShare; - if (contact && contact.avatarAttachmentId) { - TSAttachmentPointer *attachmentPointer = - [TSAttachmentPointer fetchObjectWithUniqueID:contact.avatarAttachmentId transaction:transaction]; + if (incomingMessage.contactShare.avatarAttachmentId.length > 0) { + [otherAttachmentIds addObject:incomingMessage.contactShare.avatarAttachmentId]; + } + if (incomingMessage.linkPreview.imageAttachmentId.length > 0) { + [otherAttachmentIds addObject:incomingMessage.linkPreview.imageAttachmentId]; + } + for (NSString *attachmentId in otherAttachmentIds) { + TSAttachmentPointer *_Nullable attachmentPointer = + [TSAttachmentPointer fetchObjectWithUniqueID:attachmentId transaction:transaction]; if (![attachmentPointer isKindOfClass:[TSAttachmentPointer class]]) { - OWSFailDebug(@"avatar attachmentPointer was unexpectedly nil"); - } else { - OWSLogDebug(@"downloading contact avatar for message: %lu", (unsigned long)incomingMessage.timestamp); + OWSFailDebug(@"Missing attachment pointer."); + continue; + } - [self.attachmentDownloads downloadAttachmentPointer:attachmentPointer - success:^(NSArray *attachmentStreams) { - [self.dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { + OWSLogDebug(@"Downloading attachment for message: %lu", (unsigned long)incomingMessage.timestamp); + + // Use a separate download for each attachment so that: + // + // * We update the message as each comes in. + // * Failures don't interfere with successes. + [self.attachmentDownloads downloadAttachmentPointer:attachmentPointer + success:^(NSArray *attachmentStreams) { + [self.dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { + TSAttachmentStream *_Nullable attachmentStream = attachmentStreams.firstObject; + OWSAssertDebug(attachmentStream); + if (attachmentStream && incomingMessage.quotedMessage.thumbnailAttachmentPointerId.length > 0 && + [attachmentStream.uniqueId + isEqualToString:incomingMessage.quotedMessage.thumbnailAttachmentPointerId]) { + [incomingMessage setQuotedMessageThumbnailAttachmentStream:attachmentStream]; + [incomingMessage saveWithTransaction:transaction]; + } else { + // We touch the message to trigger redraw of any views displaying it, + // since the attachment might be a contact avatar, etc. [incomingMessage touchWithTransaction:transaction]; - }]; - } - failure:^(NSError *error) { - OWSLogWarn(@"failed to fetch contact avatar for message: %lu with error: %@", - (unsigned long)incomingMessage.timestamp, - error); + } }]; - } + } + failure:^(NSError *error) { + OWSLogWarn(@"failed to download attachment for message: %lu with error: %@", + (unsigned long)incomingMessage.timestamp, + error); + }]; } + // In case we already have a read receipt for this new message (this happens sometimes). [OWSReadReceiptManager.sharedManager applyEarlyReadReceiptsForIncomingMessage:incomingMessage transaction:transaction]; diff --git a/SignalServiceKit/src/Messages/OWSMessageSender.h b/SignalServiceKit/src/Messages/OWSMessageSender.h index 61f9df926..e6c914bad 100644 --- a/SignalServiceKit/src/Messages/OWSMessageSender.h +++ b/SignalServiceKit/src/Messages/OWSMessageSender.h @@ -1,5 +1,5 @@ // -// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. // #import "DataSource.h" @@ -103,10 +103,8 @@ NS_SWIFT_NAME(MessageSender) @interface OutgoingMessagePreparer : NSObject /// Persists all necessary data to disk before sending, e.g. generate thumbnails -+ (void)prepareMessageForSending:(TSOutgoingMessage *)message - quotedThumbnailAttachments:(NSArray **)outQuotedThumbnailAttachments - contactShareAvatarAttachment:(TSAttachmentStream **)outContactShareAvatarAttachment - transaction:(YapDatabaseReadWriteTransaction *)transaction; ++ (NSArray *)prepareMessageForSending:(TSOutgoingMessage *)message + transaction:(YapDatabaseReadWriteTransaction *)transaction; /// Writes attachment to disk and applies original filename to message attributes + (void)prepareAttachments:(NSArray *)attachmentInfos diff --git a/SignalServiceKit/src/Messages/OWSMessageSender.m b/SignalServiceKit/src/Messages/OWSMessageSender.m index 977f1cc4c..303b2b74e 100644 --- a/SignalServiceKit/src/Messages/OWSMessageSender.m +++ b/SignalServiceKit/src/Messages/OWSMessageSender.m @@ -355,9 +355,7 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException"; } dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - - __block NSArray *quotedThumbnailAttachments = @[]; - __block TSAttachmentStream *_Nullable contactShareAvatarAttachment; + NSMutableArray *allAttachmentIds = [NSMutableArray new]; // This method will use a read/write transaction. This transaction // will block until any open read/write transactions are complete. @@ -372,10 +370,8 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException"; // So we're using YDB behavior to ensure this invariant, which is a bit // unorthodox. [self.dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { - [OutgoingMessagePreparer prepareMessageForSending:message - quotedThumbnailAttachments:"edThumbnailAttachments - contactShareAvatarAttachment:&contactShareAvatarAttachment - transaction:transaction]; + [allAttachmentIds + addObjectsFromArray:[OutgoingMessagePreparer prepareMessageForSending:message transaction:transaction]]; }]; NSOperationQueue *sendingQueue = [self sendingQueueForMessage:message]; @@ -386,41 +382,14 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException"; success:successHandler failure:failureHandler]; - // TODO: de-dupe attachment enqueue logic. - for (NSString *attachmentId in message.attachmentIds) { + for (NSString *attachmentId in allAttachmentIds) { OWSUploadOperation *uploadAttachmentOperation = [[OWSUploadOperation alloc] initWithAttachmentId:attachmentId dbConnection:self.dbConnection]; + // TODO: put attachment uploads on a (low priority) concurrent queue [sendMessageOperation addDependency:uploadAttachmentOperation]; [sendingQueue addOperation:uploadAttachmentOperation]; } - // Though we currently only ever expect at most one thumbnail, the proto data model - // suggests this could change. The logic is intended to work with multiple, but - // if we ever actually want to send multiple, we should do more testing. - OWSAssertDebug(quotedThumbnailAttachments.count <= 1); - for (TSAttachmentStream *thumbnailAttachment in quotedThumbnailAttachments) { - OWSAssertDebug(message.quotedMessage); - - OWSUploadOperation *uploadQuoteThumbnailOperation = - [[OWSUploadOperation alloc] initWithAttachmentId:thumbnailAttachment.uniqueId - dbConnection:self.dbConnection]; - - // TODO put attachment uploads on a (lowly) concurrent queue - [sendMessageOperation addDependency:uploadQuoteThumbnailOperation]; - [sendingQueue addOperation:uploadQuoteThumbnailOperation]; - } - - if (contactShareAvatarAttachment != nil) { - OWSAssertDebug(message.contactShare); - OWSUploadOperation *uploadAvatarOperation = - [[OWSUploadOperation alloc] initWithAttachmentId:contactShareAvatarAttachment.uniqueId - dbConnection:self.dbConnection]; - - // TODO put attachment uploads on a (lowly) concurrent queue - [sendMessageOperation addDependency:uploadAvatarOperation]; - [sendingQueue addOperation:uploadAvatarOperation]; - } - [sendingQueue addOperation:sendMessageOperation]; }); } @@ -1838,22 +1807,45 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException"; #pragma mark - -+ (void)prepareMessageForSending:(TSOutgoingMessage *)message - quotedThumbnailAttachments:(NSArray **)outQuotedThumbnailAttachments - contactShareAvatarAttachment:(TSAttachmentStream *_Nullable *)outContactShareAvatarAttachment - transaction:(YapDatabaseReadWriteTransaction *)transaction ++ (NSArray *)prepareMessageForSending:(TSOutgoingMessage *)message + transaction:(YapDatabaseReadWriteTransaction *)transaction { + OWSAssertDebug(message); + OWSAssertDebug(transaction); + + NSMutableArray *attachmentIds = [NSMutableArray new]; + + if (message.attachmentIds) { + [attachmentIds addObjectsFromArray:message.attachmentIds]; + } + if (message.quotedMessage) { - *outQuotedThumbnailAttachments = + // Though we currently only ever expect at most one thumbnail, the proto data model + // suggests this could change. The logic is intended to work with multiple, but + // if we ever actually want to send multiple, we should do more testing. + NSArray *quotedThumbnailAttachments = [message.quotedMessage createThumbnailAttachmentsIfNecessaryWithTransaction:transaction]; + for (TSAttachmentStream *attachment in quotedThumbnailAttachments) { + [attachmentIds addObject:attachment.uniqueId]; + } } if (message.contactShare.avatarAttachmentId != nil) { - TSAttachment *avatarAttachment = [message.contactShare avatarAttachmentWithTransaction:transaction]; - if ([avatarAttachment isKindOfClass:[TSAttachmentStream class]]) { - *outContactShareAvatarAttachment = (TSAttachmentStream *)avatarAttachment; + TSAttachment *attachment = [message.contactShare avatarAttachmentWithTransaction:transaction]; + if ([attachment isKindOfClass:[TSAttachmentStream class]]) { + [attachmentIds addObject:attachment.uniqueId]; + } else { + OWSFailDebug(@"unexpected avatarAttachment: %@", attachment); + } + } + + if (message.linkPreview.imageAttachmentId != nil) { + TSAttachment *attachment = + [TSAttachment fetchObjectWithUniqueID:message.linkPreview.imageAttachmentId transaction:transaction]; + if ([attachment isKindOfClass:[TSAttachmentStream class]]) { + [attachmentIds addObject:attachment.uniqueId]; } else { - OWSFailDebug(@"unexpected avatarAttachment: %@", avatarAttachment); + OWSFailDebug(@"unexpected attachment: %@", attachment); } } @@ -1861,6 +1853,8 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException"; [message saveWithTransaction:transaction]; // When we start a message send, all "failed" recipients should be marked as "sending". [message updateWithMarkingAllUnsentRecipientsAsSendingWithTransaction:transaction]; + + return attachmentIds; } + (void)prepareAttachments:(NSArray *)attachmentInfos diff --git a/SignalServiceKit/src/Protos/Generated/SSKProto.swift b/SignalServiceKit/src/Protos/Generated/SSKProto.swift index 9af1d765f..ffafe9713 100644 --- a/SignalServiceKit/src/Protos/Generated/SSKProto.swift +++ b/SignalServiceKit/src/Protos/Generated/SSKProto.swift @@ -2864,9 +2864,7 @@ extension SSKProtoDataMessagePreview.SSKProtoDataMessagePreviewBuilder { builder.setQuote(_value) } builder.setContact(contact) - if let _value = preview { - builder.setPreview(_value) - } + builder.setPreview(preview) return builder } @@ -2924,8 +2922,14 @@ extension SSKProtoDataMessagePreview.SSKProtoDataMessagePreviewBuilder { proto.contact = wrappedItems.map { $0.proto } } - @objc public func setPreview(_ valueParam: SSKProtoDataMessagePreview) { - proto.preview = valueParam.proto + @objc public func addPreview(_ valueParam: SSKProtoDataMessagePreview) { + var items = proto.preview + items.append(valueParam.proto) + proto.preview = items + } + + @objc public func setPreview(_ wrappedItems: [SSKProtoDataMessagePreview]) { + proto.preview = wrappedItems.map { $0.proto } } @objc public func build() throws -> SSKProtoDataMessage { @@ -2947,7 +2951,7 @@ extension SSKProtoDataMessagePreview.SSKProtoDataMessagePreviewBuilder { @objc public let contact: [SSKProtoDataMessageContact] - @objc public let preview: SSKProtoDataMessagePreview? + @objc public let preview: [SSKProtoDataMessagePreview] @objc public var body: String? { guard proto.hasBody else { @@ -2995,7 +2999,7 @@ extension SSKProtoDataMessagePreview.SSKProtoDataMessagePreviewBuilder { group: SSKProtoGroupContext?, quote: SSKProtoDataMessageQuote?, contact: [SSKProtoDataMessageContact], - preview: SSKProtoDataMessagePreview?) { + preview: [SSKProtoDataMessagePreview]) { self.proto = proto self.attachments = attachments self.group = group @@ -3031,10 +3035,8 @@ extension SSKProtoDataMessagePreview.SSKProtoDataMessagePreviewBuilder { var contact: [SSKProtoDataMessageContact] = [] contact = try proto.contact.map { try SSKProtoDataMessageContact.parseProto($0) } - var preview: SSKProtoDataMessagePreview? = nil - if proto.hasPreview { - preview = try SSKProtoDataMessagePreview.parseProto(proto.preview) - } + var preview: [SSKProtoDataMessagePreview] = [] + preview = try proto.preview.map { try SSKProtoDataMessagePreview.parseProto($0) } // MARK: - Begin Validation Logic for SSKProtoDataMessage - diff --git a/SignalServiceKit/src/Protos/Generated/SignalService.pb.swift b/SignalServiceKit/src/Protos/Generated/SignalService.pb.swift index 713b635da..f4e9e91db 100644 --- a/SignalServiceKit/src/Protos/Generated/SignalService.pb.swift +++ b/SignalServiceKit/src/Protos/Generated/SignalService.pb.swift @@ -634,14 +634,10 @@ struct SignalServiceProtos_DataMessage { set {_uniqueStorage()._contact = newValue} } - var preview: SignalServiceProtos_DataMessage.Preview { - get {return _storage._preview ?? SignalServiceProtos_DataMessage.Preview()} + var preview: [SignalServiceProtos_DataMessage.Preview] { + get {return _storage._preview} set {_uniqueStorage()._preview = newValue} } - /// Returns true if `preview` has been explicitly set. - var hasPreview: Bool {return _storage._preview != nil} - /// Clears the value of `preview`. Subsequent reads from it will return its default value. - mutating func clearPreview() {_uniqueStorage()._preview = nil} var unknownFields = SwiftProtobuf.UnknownStorage() @@ -2834,7 +2830,7 @@ extension SignalServiceProtos_DataMessage: SwiftProtobuf.Message, SwiftProtobuf. var _timestamp: UInt64? = nil var _quote: SignalServiceProtos_DataMessage.Quote? = nil var _contact: [SignalServiceProtos_DataMessage.Contact] = [] - var _preview: SignalServiceProtos_DataMessage.Preview? = nil + var _preview: [SignalServiceProtos_DataMessage.Preview] = [] static let defaultInstance = _StorageClass() @@ -2875,7 +2871,7 @@ extension SignalServiceProtos_DataMessage: SwiftProtobuf.Message, SwiftProtobuf. case 7: try decoder.decodeSingularUInt64Field(value: &_storage._timestamp) case 8: try decoder.decodeSingularMessageField(value: &_storage._quote) case 9: try decoder.decodeRepeatedMessageField(value: &_storage._contact) - case 10: try decoder.decodeSingularMessageField(value: &_storage._preview) + case 10: try decoder.decodeRepeatedMessageField(value: &_storage._preview) default: break } } @@ -2911,8 +2907,8 @@ extension SignalServiceProtos_DataMessage: SwiftProtobuf.Message, SwiftProtobuf. if !_storage._contact.isEmpty { try visitor.visitRepeatedMessageField(value: _storage._contact, fieldNumber: 9) } - if let v = _storage._preview { - try visitor.visitSingularMessageField(value: v, fieldNumber: 10) + if !_storage._preview.isEmpty { + try visitor.visitRepeatedMessageField(value: _storage._preview, fieldNumber: 10) } } try unknownFields.traverse(visitor: &visitor)