Fix issues around link previews.

This commit is contained in:
Matthew Chen 2019-01-17 11:56:21 -05:00
parent dd7ff1360a
commit 2dcc79fbca
12 changed files with 241 additions and 181 deletions

View File

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

View File

@ -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 <SignalServiceKit/TSInvalidIdentityKeyReceivingErrorMessage.h>
#import <SignalServiceKit/TSThread.h>
#import <SignalServiceKit/UIImage+OWS.h>
#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

View File

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

View File

@ -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<SignalAttachment *> *)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<OWSOutgoingAttachmentInfo *> *attachmentInfos = [NSMutableArray new];
for (SignalAttachment *attachment in attachments) {

View File

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

View File

@ -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..<maxLineCount])
result = components.joined(separator: "\n")
}
let maxCharacterCount = 2048
if result.count > 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<AnyObject, AnyObject> = 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<AnyObject, OWSLinkPreviewInfo> = NSCache()
private static var linkPreviewDraftCache: NSCache<AnyObject, OWSLinkPreviewDraft> = 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 {
// <meta property="og:image" content="https://i.ytimg.com/vi/tP-Ipsat90c/maxresdefault.jpg">
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: "<meta property=\"og:title\" content=\"([^\"]+)\">", text: linkText)
var title: String?
if let rawTitle = NSRegularExpression.parseFirstMatch(pattern: "<meta property=\"og:title\" content=\"([^\"]+)\">", 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: "<meta property=\"og:image\" content=\"([^\"]+)\">", 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)
})
}
}

View File

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

View File

@ -1437,55 +1437,53 @@ 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<TSAttachmentStream *> *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<NSString *> *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);
[self.attachmentDownloads downloadAttachmentPointer:attachmentPointer
success:^(NSArray<TSAttachmentStream *> *attachmentStreams) {
[self.dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
[incomingMessage touchWithTransaction:transaction];
}];
}
failure:^(NSError *error) {
OWSLogWarn(@"failed to fetch contact avatar for message: %lu with error: %@",
(unsigned long)incomingMessage.timestamp,
error);
}];
OWSFailDebug(@"Missing attachment pointer.");
continue;
}
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<TSAttachmentStream *> *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 {
[incomingMessage touchWithTransaction:transaction];
}
}];
}
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];

View File

@ -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<TSAttachmentStream *> **)outQuotedThumbnailAttachments
contactShareAvatarAttachment:(TSAttachmentStream **)outContactShareAvatarAttachment
transaction:(YapDatabaseReadWriteTransaction *)transaction;
+ (NSArray<NSString *> *)prepareMessageForSending:(TSOutgoingMessage *)message
transaction:(YapDatabaseReadWriteTransaction *)transaction;
/// Writes attachment to disk and applies original filename to message attributes
+ (void)prepareAttachments:(NSArray<OWSOutgoingAttachmentInfo *> *)attachmentInfos

View File

@ -355,9 +355,7 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
}
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
__block NSArray<TSAttachmentStream *> *quotedThumbnailAttachments = @[];
__block TSAttachmentStream *_Nullable contactShareAvatarAttachment;
NSMutableArray<NSString *> *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:&quotedThumbnailAttachments
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<TSAttachmentStream *> **)outQuotedThumbnailAttachments
contactShareAvatarAttachment:(TSAttachmentStream *_Nullable *)outContactShareAvatarAttachment
transaction:(YapDatabaseReadWriteTransaction *)transaction
+ (NSArray<NSString *> *)prepareMessageForSending:(TSOutgoingMessage *)message
transaction:(YapDatabaseReadWriteTransaction *)transaction
{
OWSAssertDebug(message);
OWSAssertDebug(transaction);
NSMutableArray<NSString *> *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<TSAttachmentStream *> *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: %@", avatarAttachment);
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 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<OWSOutgoingAttachmentInfo *> *)attachmentInfos

View File

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

View File

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