From 31ea64bdaf3fa5a4a13f337ed7d90345870cf37b Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Tue, 15 Jan 2019 15:52:08 -0500 Subject: [PATCH 1/5] Build link previews. --- .../ConversationViewController.m | 1 + .../ViewControllers/DebugUI/DebugUIMessages.m | 7 +- Signal/src/network/GiphyAPI.swift | 28 +- Signal/src/network/GiphyDownloader.swift | 4 +- Signal/src/util/Pastelog.m | 4 +- SignalMessaging/utils/ThreadUtil.h | 4 +- SignalMessaging/utils/ThreadUtil.m | 100 ++++- .../Interactions/OWSLinkPreview.swift | 385 +++++++++++++++++- .../src/Messages/Interactions/TSMessage.h | 2 + .../src/Messages/Interactions/TSMessage.m | 13 + .../Messages/Interactions/TSOutgoingMessage.h | 3 +- .../Messages/Interactions/TSOutgoingMessage.m | 35 +- .../src/Messages/OWSMessageSender.m | 60 +++ .../src/Network/ReverseProxy.swift | 54 +++ 14 files changed, 645 insertions(+), 55 deletions(-) create mode 100644 SignalServiceKit/src/Network/ReverseProxy.swift diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m index 798311d1a..7b45cbdc7 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m +++ b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m @@ -3983,6 +3983,7 @@ typedef enum : NSUInteger { message = [ThreadUtil enqueueMessageWithText:text inThread:self.thread quotedReplyModel:self.inputToolbar.quotedReply + linkPreview:nil transaction:transaction]; }]; } diff --git a/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m b/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m index 9a9a62718..b0a086305 100644 --- a/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m +++ b/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m @@ -359,7 +359,11 @@ NS_ASSUME_NONNULL_BEGIN NSString *text = [[[@(counter) description] stringByAppendingString:@" "] stringByAppendingString:randomText]; __block TSOutgoingMessage *message; [self.dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { - message = [ThreadUtil enqueueMessageWithText:text inThread:thread quotedReplyModel:nil transaction:transaction]; + message = [ThreadUtil enqueueMessageWithText:text + inThread:thread + quotedReplyModel:nil + linkPreview:nil + transaction:transaction]; }]; OWSLogError(@"sendTextMessageInThread timestamp: %llu.", message.timestamp); } @@ -3884,6 +3888,7 @@ typedef OWSContact * (^OWSContactBlock)(YapDatabaseReadWriteTransaction *transac [ThreadUtil enqueueMessageWithText:[@(counter) description] inThread:thread quotedReplyModel:nil + linkPreview:nil transaction:transaction]; }]; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)1.f * NSEC_PER_SEC), dispatch_get_main_queue(), ^{ diff --git a/Signal/src/network/GiphyAPI.swift b/Signal/src/network/GiphyAPI.swift index 3ca844f00..25121883c 100644 --- a/Signal/src/network/GiphyAPI.swift +++ b/Signal/src/network/GiphyAPI.swift @@ -1,5 +1,5 @@ // -// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. // import Foundation @@ -280,32 +280,8 @@ extension GiphyError: LocalizedError { private let kGiphyBaseURL = "https://api.giphy.com/" - public class func giphySessionConfiguration() -> URLSessionConfiguration { - let configuration = URLSessionConfiguration.ephemeral - let proxyHost = "giphy-proxy-production.whispersystems.org" - let proxyPort = 80 - configuration.connectionProxyDictionary = [ - "HTTPEnable": 1, - "HTTPProxy": proxyHost, - "HTTPPort": proxyPort, - "HTTPSEnable": 1, - "HTTPSProxy": proxyHost, - "HTTPSPort": proxyPort - ] - return configuration - } - private func giphyAPISessionManager() -> AFHTTPSessionManager? { - guard let baseUrl = NSURL(string: kGiphyBaseURL) else { - Logger.error("Invalid base URL.") - return nil - } - let sessionManager = AFHTTPSessionManager(baseURL: baseUrl as URL, - sessionConfiguration: GiphyAPI.giphySessionConfiguration()) - sessionManager.requestSerializer = AFJSONRequestSerializer() - sessionManager.responseSerializer = AFJSONResponseSerializer() - - return sessionManager + return ReverseProxy.jsonSessionManager(baseUrl: kGiphyBaseURL) } // MARK: Search diff --git a/Signal/src/network/GiphyDownloader.swift b/Signal/src/network/GiphyDownloader.swift index 0556ff6cc..86c94ae91 100644 --- a/Signal/src/network/GiphyDownloader.swift +++ b/Signal/src/network/GiphyDownloader.swift @@ -1,5 +1,5 @@ // -// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. // import Foundation @@ -415,7 +415,7 @@ extension URLSessionTask { private lazy var giphyDownloadSession: URLSession = { AssertIsOnMainThread() - let configuration = GiphyAPI.giphySessionConfiguration() + let configuration = ReverseProxy.sessionConfiguration() configuration.urlCache = nil configuration.requestCachePolicy = .reloadIgnoringCacheData configuration.httpMaximumConnectionsPerHost = 10 diff --git a/Signal/src/util/Pastelog.m b/Signal/src/util/Pastelog.m index a70559829..f737cf06b 100644 --- a/Signal/src/util/Pastelog.m +++ b/Signal/src/util/Pastelog.m @@ -1,5 +1,5 @@ // -// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. // #import "Pastelog.h" @@ -591,6 +591,7 @@ typedef void (^DebugLogUploadFailure)(DebugLogUploader *uploader, NSError *error [ThreadUtil enqueueMessageWithText:url.absoluteString inThread:thread quotedReplyModel:nil + linkPreview:nil transaction:transaction]; }]; }); @@ -615,6 +616,7 @@ typedef void (^DebugLogUploadFailure)(DebugLogUploader *uploader, NSError *error [ThreadUtil enqueueMessageWithText:url.absoluteString inThread:thread quotedReplyModel:nil + linkPreview:nil transaction:transaction]; }]; } else { diff --git a/SignalMessaging/utils/ThreadUtil.h b/SignalMessaging/utils/ThreadUtil.h index fe6348fc6..6a20274ee 100644 --- a/SignalMessaging/utils/ThreadUtil.h +++ b/SignalMessaging/utils/ThreadUtil.h @@ -1,11 +1,12 @@ // -// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. // NS_ASSUME_NONNULL_BEGIN @class OWSBlockingManager; @class OWSContactsManager; +@class OWSLinkPreview; @class OWSMessageSender; @class OWSUnreadIndicator; @class SignalAttachment; @@ -47,6 +48,7 @@ NS_ASSUME_NONNULL_BEGIN + (TSOutgoingMessage *)enqueueMessageWithText:(NSString *)text inThread:(TSThread *)thread quotedReplyModel:(nullable OWSQuotedReplyModel *)quotedReplyModel + linkPreview:(nullable OWSLinkPreview *)linkPreview transaction:(YapDatabaseReadTransaction *)transaction; + (TSOutgoingMessage *)enqueueMessageWithAttachment:(SignalAttachment *)attachment diff --git a/SignalMessaging/utils/ThreadUtil.m b/SignalMessaging/utils/ThreadUtil.m index 7a071b428..7d46bc6dd 100644 --- a/SignalMessaging/utils/ThreadUtil.m +++ b/SignalMessaging/utils/ThreadUtil.m @@ -8,6 +8,7 @@ #import "OWSQuotedReplyModel.h" #import "OWSUnreadIndicator.h" #import "TSUnreadIndicatorInteraction.h" +#import #import #import #import @@ -65,9 +66,101 @@ NS_ASSUME_NONNULL_BEGIN #pragma mark - Durable Message Enqueue +// TODO: Move this elsewhere. ++ (void)ensureLinkPreviewForMessage:(TSOutgoingMessage *)message completion:(dispatch_block_t)completion +{ + OWSAssert(message); + OWSAssert(completion); + + if (message.linkPreview != nil) { + // Message already has link preview. + completion(); + return; + } + + [OWSLinkPreview + tryToBuildPreviewInfoForMessageBodyText:message.body + completion:^(OWSLinkPreviewInfo *_Nullable linkPreviewInfo) { + if (!linkPreviewInfo) { + completion(); + return; + } + + [self.dbConnection + asyncReadWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { + NSError *linkPreviewError; + OWSLinkPreview *_Nullable linkPreview = [OWSLinkPreview + buildValidatedLinkPreviewFromInfo:linkPreviewInfo + transaction:transaction + error:&linkPreviewError]; + if (linkPreviewError + && ![OWSLinkPreview isNoPreviewError:linkPreviewError]) { + OWSFailDebug(@"linkPreviewError: %@", linkPreviewError); + completion(); + return; + } + if (!linkPreview) { + OWSFailDebug(@"Missing linkPreview."); + completion(); + return; + } + + [message updateWithLinkPreview:linkPreview transaction:transaction]; + completion(); + }]; + }]; +} + +//+ (TSOutgoingMessage *)enqueueMessageWithText:(NSString *)text +// inThread:(TSThread *)thread +// quotedReplyModel:(nullable OWSQuotedReplyModel *)quotedReplyModel +// linkPreview:(nullable OWSLinkPreview *)linkPreview +// transaction:(YapDatabaseReadTransaction *)transaction +//{ +// OWSDisappearingMessagesConfiguration *configuration = +// [OWSDisappearingMessagesConfiguration fetchObjectWithUniqueID:thread.uniqueId transaction:transaction]; +// +// uint32_t expiresInSeconds = (configuration.isEnabled ? configuration.durationSeconds : 0); +// +// TSOutgoingMessage *message = +// [TSOutgoingMessage outgoingMessageInThread:thread +// messageBody:text +// attachmentId:nil +// expiresInSeconds:expiresInSeconds +// quotedMessage:[quotedReplyModel buildQuotedMessageForSending] +// linkPreview:linkPreview]; +// +// [BenchManager benchAsyncWithTitle:@"Saving outgoing message" block:^(void (^benchmarkCompletion)(void)) { +// // To avoid blocking the send flow, we dispatch an async write from within this read transaction +// AnyPromise *promise = [[AnyPromise promiseWithResolverBlock:^(PMKResolver resolve) { +// [self.dbConnection asyncReadWriteWithBlock:^(YapDatabaseReadWriteTransaction *writeTransaction) { +// [message saveWithTransaction:writeTransaction]; +// } +// completionBlock:^{ +// resolve(@(1)); +// }]; +// }].thenInBackground(^{ +// return [AnyPromise promiseWithResolverBlock:^(PMKResolver resolve) { +// [ThreadUtil ensureLinkPreviewForMessage:message +// completion:^{ +// resolve(@(1)); +// }]; +// }]; +// }].thenInBackground(^{ +// [self.messageSenderJobQueue addMessage:message +// transaction:writeTransaction]; +// }].thenInBackground(^{ +// benchmarkCompletion(); +// }) retainUntilComplete]; +// }]; +// +// return message; +//} + + (TSOutgoingMessage *)enqueueMessageWithText:(NSString *)text inThread:(TSThread *)thread quotedReplyModel:(nullable OWSQuotedReplyModel *)quotedReplyModel + linkPreview:(nullable OWSLinkPreview *)linkPreview transaction:(YapDatabaseReadTransaction *)transaction { OWSDisappearingMessagesConfiguration *configuration = @@ -80,7 +173,8 @@ NS_ASSUME_NONNULL_BEGIN messageBody:text attachmentId:nil expiresInSeconds:expiresInSeconds - quotedMessage:[quotedReplyModel buildQuotedMessageForSending]]; + quotedMessage:[quotedReplyModel buildQuotedMessageForSending] + linkPreview:linkPreview]; [BenchManager benchAsyncWithTitle:@"Saving outgoing message" block:^(void (^benchmarkCompletion)(void)) { // To avoid blocking the send flow, we dispatch an async write from within this read transaction @@ -193,6 +287,7 @@ NS_ASSUME_NONNULL_BEGIN // MARK: Non-Durable Sending +// We might want to generate a link preview here. + (TSOutgoingMessage *)sendMessageNonDurablyWithText:(NSString *)text inThread:(TSThread *)thread quotedReplyModel:(nullable OWSQuotedReplyModel *)quotedReplyModel @@ -213,7 +308,8 @@ NS_ASSUME_NONNULL_BEGIN messageBody:text attachmentId:nil expiresInSeconds:expiresInSeconds - quotedMessage:[quotedReplyModel buildQuotedMessageForSending]]; + quotedMessage:[quotedReplyModel buildQuotedMessageForSending] + linkPreview:nil]; [messageSender sendMessage:message success:successHandler failure:failureHandler]; diff --git a/SignalServiceKit/src/Messages/Interactions/OWSLinkPreview.swift b/SignalServiceKit/src/Messages/Interactions/OWSLinkPreview.swift index 716ea807a..f6c806ab5 100644 --- a/SignalServiceKit/src/Messages/Interactions/OWSLinkPreview.swift +++ b/SignalServiceKit/src/Messages/Interactions/OWSLinkPreview.swift @@ -7,12 +7,48 @@ import Foundation @objc public enum LinkPreviewError: Int, Error { case invalidInput - case assertionFailure case noPreview } -@objc(OWSLinkPreview) +// MARK: - OWSLinkPreviewInfo + +// This contains the info for a link preview "draft". +public class OWSLinkPreviewInfo: NSObject { + @objc + public var urlString: String + + @objc + public var title: String? + + @objc + public var imageFilePath: String? + + @objc + public init(urlString: String, title: String?, imageFilePath: String? = nil) { + self.urlString = urlString + self.title = title + self.imageFilePath = imageFilePath + + super.init() + } + + fileprivate func isValid() -> Bool { + var hasTitle = false + if let titleValue = title { + hasTitle = titleValue.count > 0 + } + let hasImage = imageFilePath != nil + return hasTitle || hasImage + } +} + +// MARK: - OWSLinkPreview + +@objc public class OWSLinkPreview: MTLModel { + @objc + public static let featureEnabled = true + @objc public var urlString: String? @@ -31,6 +67,11 @@ public class OWSLinkPreview: MTLModel { super.init() } + @objc + public override init() { + super.init() + } + @objc public required init!(coder: NSCoder) { super.init(coder: coder) @@ -53,6 +94,9 @@ public class OWSLinkPreview: MTLModel { public class func buildValidatedLinkPreview(dataMessage: SSKProtoDataMessage, body: String?, transaction: YapDatabaseReadWriteTransaction) throws -> OWSLinkPreview { + guard OWSLinkPreview.featureEnabled else { + throw LinkPreviewError.noPreview + } guard let previewProto = dataMessage.preview else { throw LinkPreviewError.noPreview } @@ -73,7 +117,11 @@ public class OWSLinkPreview: MTLModel { throw LinkPreviewError.invalidInput } - // TODO: Verify that url host is in whitelist. + guard isValidLinkUrl(urlString) else { + Logger.verbose("Invalid link URL \(urlString).") + Logger.error("Invalid link URL.") + throw LinkPreviewError.invalidInput + } let title: String? = previewProto.title?.trimmingCharacters(in: .whitespacesAndNewlines) @@ -88,17 +136,70 @@ public class OWSLinkPreview: MTLModel { } } + let linkPreview = OWSLinkPreview(urlString: urlString, title: title, imageAttachmentId: imageAttachmentId) + + guard linkPreview.isValid() else { + owsFailDebug("Preview has neither title nor image.") + throw LinkPreviewError.invalidInput + } + + return linkPreview + } + + @objc + public class func buildValidatedLinkPreview(fromInfo info: OWSLinkPreviewInfo, + transaction: YapDatabaseReadWriteTransaction) throws -> OWSLinkPreview { + guard OWSLinkPreview.featureEnabled else { + throw LinkPreviewError.noPreview + } + let imageAttachmentId = OWSLinkPreview.saveAttachmentIfPossible(forFilePath: info.imageFilePath, + transaction: transaction) + + let linkPreview = OWSLinkPreview(urlString: info.urlString, title: info.title, imageAttachmentId: imageAttachmentId) + + guard linkPreview.isValid() else { + owsFailDebug("Preview has neither title nor image.") + throw LinkPreviewError.invalidInput + } + + return linkPreview + } + + private class func saveAttachmentIfPossible(forFilePath filePath: String?, + transaction: YapDatabaseReadWriteTransaction) -> String? { + guard let filePath = filePath else { + return nil + } + guard let fileSize = OWSFileSystem.fileSize(ofPath: filePath) else { + owsFailDebug("Unknown file size for path: \(filePath)") + return nil + } + guard fileSize.uint32Value > 0 else { + owsFailDebug("Invalid file size for path: \(filePath)") + return nil + } + let filename = (filePath as NSString).lastPathComponent + let fileExtension = (filename as NSString).pathExtension + guard fileExtension.count > 0 else { + owsFailDebug("Invalid file extension for path: \(filePath)") + return nil + } + guard let contentType = MIMETypeUtil.mimeType(forFileExtension: fileExtension) else { + owsFailDebug("Invalid content type for path: \(filePath)") + return nil + } + let attachment = TSAttachmentStream(contentType: contentType, byteCount: fileSize.uint32Value, sourceFilename: nil, caption: nil, albumMessageId: nil) + attachment.save(with: transaction) + return attachment.uniqueId + } + + private func isValid() -> Bool { var hasTitle = false if let titleValue = title { hasTitle = titleValue.count > 0 } let hasImage = imageAttachmentId != nil - if !hasTitle && !hasImage { - Logger.error("Preview has neither title nor image.") - throw LinkPreviewError.invalidInput - } - - return OWSLinkPreview(urlString: urlString, title: title, imageAttachmentId: imageAttachmentId) + return hasTitle || hasImage } @objc @@ -117,20 +218,30 @@ public class OWSLinkPreview: MTLModel { // MARK: - Domain Whitelist private static let linkDomainWhitelist = [ - "youtube.com", - "reddit.com", - "imgur.com", - "instagram.com" + "youtube.com", + "reddit.com", + "imgur.com", + "instagram.com" ] private static let mediaDomainWhitelist = [ "ytimg.com", "cdninstagram.com" - ] + ] private static let protocolWhitelist = [ "https" - ] + ] + + // *.giphy.com + // *.youtube.com + // *.youtu.be + // *.ytimg.com + // *.reddit.com + // *.reddi.it + // *.imgur.com + // *.instagram.com + // *.cdninstagram.com @objc public class func isValidLinkUrl(_ urlString: String) -> Bool { @@ -147,7 +258,7 @@ public class OWSLinkPreview: MTLModel { return false } return isUrlInDomainWhitelist(url: url, - domainWhitelist: OWSLinkPreview.linkDomainWhitelist + OWSLinkPreview.mediaDomainWhitelist) + domainWhitelist: OWSLinkPreview.linkDomainWhitelist + OWSLinkPreview.mediaDomainWhitelist) } private class func isUrlInDomainWhitelist(url: URL, domainWhitelist: [String]) -> Bool { @@ -176,19 +287,257 @@ public class OWSLinkPreview: MTLModel { return false } + // MARK: - Serial Queue + + private static let serialQueue = DispatchQueue(label: "org.signal.linkPreview") + + private class func assertIsOnSerialQueue() { + if _isDebugAssertConfiguration(), #available(iOS 10.0, *) { + assertOnQueue(serialQueue) + } + } + // MARK: - Text Parsing - @objc - public class func previewUrl(forMessageBodyText body: String?) -> String? { + // This cache should only be accessed on serialQueue. + private static var previewUrlCache: NSCache = NSCache() + + private class func previewUrl(forMessageBodyText body: String?) -> String? { + assertIsOnSerialQueue() + + guard OWSLinkPreview.featureEnabled else { + return nil + } + guard let body = body else { return nil } + if let cachedUrl = previewUrlCache.object(forKey: body as AnyObject) as? String { + Logger.verbose("URL parsing cache hit.") + guard cachedUrl.count > 0 else { + return nil + } + return cachedUrl + } let components = body.components(separatedBy: .whitespacesAndNewlines) for component in components { if isValidLinkUrl(component) { + previewUrlCache.setObject(component as AnyObject, forKey: body as AnyObject) return component } } return nil } + + // MARK: - Preview Construction + + // This cache should only be accessed on serialQueue. + private static var linkPreviewInfoCache: 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) { + guard OWSLinkPreview.featureEnabled else { + completion(nil) + return + } + guard let body = body 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) { + Logger.verbose("Link preview info cache hit.") + completion(cachedInfo) + return + } + downloadContents(ofUrl: previewUrl, completion: { (data) in + DispatchQueue.global().async { + guard let data = data else { + completion(nil) + return + } + parse(linkData: data, linkUrlString: previewUrl) { (linkPreviewInfo) in + guard let linkPreviewInfo = linkPreviewInfo else { + completion(nil) + return + } + guard linkPreviewInfo.isValid() else { + completion(nil) + return + } + serialQueue.async { + previewUrlCache.setObject(linkPreviewInfo, forKey: previewUrl as AnyObject) + + DispatchQueue.global().async { + completion(linkPreviewInfo) + } + } + } + } + }) + } + } + + private class func downloadContents(ofUrl url: String, + completion: @escaping (Data?) -> Void, + remainingRetries: UInt = 3) { + + Logger.verbose("url: \(url)") + + guard let sessionManager: AFHTTPSessionManager = ReverseProxy.sessionManager(baseUrl: nil) else { + owsFailDebug("Couldn't create session manager.") + completion(nil) + return + } + sessionManager.requestSerializer = AFHTTPRequestSerializer() + sessionManager.responseSerializer = AFHTTPResponseSerializer() + + // Remove all headers from the request. + for headerField in sessionManager.requestSerializer.httpRequestHeaders.keys { + sessionManager.requestSerializer.setValue(nil, forHTTPHeaderField: headerField) + } + + sessionManager.get(url, + parameters: {}, + progress: nil, + success: { _, value in + + guard let data = value as? Data else { + Logger.warn("Result is not data: \(type(of: value)).") + completion(nil) + return + } + completion(data) + }, + failure: { _, error in + Logger.verbose("Error: \(error)") + + guard isRetryable(error: error) else { + Logger.warn("Error is not retryable.") + completion(nil) + return + } + + guard remainingRetries > 0 else { + Logger.warn("No more retries.") + completion(nil) + return + } + OWSLinkPreview.downloadContents(ofUrl: url, completion: completion, remainingRetries: remainingRetries - 1) + }) + + } + + private class func isRetryable(error: Error) -> Bool { + let nsError = error as NSError + if nsError.domain == kCFErrorDomainCFNetwork as String { + // Network failures are retried. + return true + } + return false + } + + // Example: + // + // + // + private class func parse(linkData: Data, + linkUrlString: String, + completion: @escaping (OWSLinkPreviewInfo?) -> 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 = parseFirstMatch(pattern: "", text: linkText) + Logger.verbose("title: \(String(describing: title))") + + guard let imageUrlString = parseFirstMatch(pattern: "", text: linkText) else { + return completion(OWSLinkPreviewInfo(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)) + } + 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)) + } + let kValidMimeTypes = [ + OWSMimeTypeImagePng, + OWSMimeTypeImageJpeg + ] + guard kValidMimeTypes.contains(imageMimeType) else { + Logger.error("Image URL has invalid content type: \(imageMimeType).") + return completion(OWSLinkPreviewInfo(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)) + } + 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)) + } + // NOTE: imageSize(forFilePath:...) will call ows_isValidImage(...). + let imageSize = NSData.imageSize(forFilePath: imageFilePath, mimeType: imageMimeType) + let kMaxImageSize: CGFloat = 2048 + guard imageSize.width > 0, + imageSize.height > 0, + imageSize.width < kMaxImageSize, + imageSize.height < kMaxImageSize else { + Logger.error("Image has invalid size: \(imageSize).") + return completion(OWSLinkPreviewInfo(urlString: linkUrlString, title: title)) + } + + let linkPreviewInfo = OWSLinkPreviewInfo(urlString: linkUrlString, title: title, imageFilePath: imageFilePath) + completion(linkPreviewInfo) + }) + } + + private class func parseFirstMatch(pattern: String, + text: String) -> String? { + do { + let regex = try NSRegularExpression(pattern: pattern) + guard let match = regex.firstMatch(in: text, + options: [], + range: NSRange(location: 0, length: text.count)) else { + return nil + } + let matchRange = match.range(at: 1) + guard let textRange = Range(matchRange, in: text) else { + owsFailDebug("Invalid match.") + return nil + } + let substring = String(text[textRange]) + return substring + } catch { + Logger.error("Error: \(error)") + return nil + } + } } diff --git a/SignalServiceKit/src/Messages/Interactions/TSMessage.h b/SignalServiceKit/src/Messages/Interactions/TSMessage.h index cf353f3ca..55297e154 100644 --- a/SignalServiceKit/src/Messages/Interactions/TSMessage.h +++ b/SignalServiceKit/src/Messages/Interactions/TSMessage.h @@ -61,6 +61,8 @@ NS_ASSUME_NONNULL_BEGIN - (void)updateWithExpireStartedAt:(uint64_t)expireStartedAt transaction:(YapDatabaseReadWriteTransaction *)transaction; +- (void)updateWithLinkPreview:(OWSLinkPreview *)linkPreview transaction:(YapDatabaseReadWriteTransaction *)transaction; + @end NS_ASSUME_NONNULL_END diff --git a/SignalServiceKit/src/Messages/Interactions/TSMessage.m b/SignalServiceKit/src/Messages/Interactions/TSMessage.m index a1a6979b2..53def73d2 100644 --- a/SignalServiceKit/src/Messages/Interactions/TSMessage.m +++ b/SignalServiceKit/src/Messages/Interactions/TSMessage.m @@ -48,6 +48,8 @@ static const NSUInteger OWSMessageSchemaVersion = 4; */ @property (nonatomic, readonly) NSUInteger schemaVersion; +@property (nonatomic, nullable) OWSLinkPreview *linkPreview; + @end #pragma mark - @@ -419,6 +421,17 @@ static const NSUInteger OWSMessageSchemaVersion = 4; }]; } +- (void)updateWithLinkPreview:(OWSLinkPreview *)linkPreview transaction:(YapDatabaseReadWriteTransaction *)transaction +{ + OWSAssertDebug(linkPreview); + OWSAssertDebug(transaction); + + [self applyChangeToSelfAndLatestCopy:transaction + changeBlock:^(TSOutgoingMessage *message) { + message.linkPreview = linkPreview; + }]; +} + @end NS_ASSUME_NONNULL_END diff --git a/SignalServiceKit/src/Messages/Interactions/TSOutgoingMessage.h b/SignalServiceKit/src/Messages/Interactions/TSOutgoingMessage.h index 1bf16def3..b007d7fc4 100644 --- a/SignalServiceKit/src/Messages/Interactions/TSOutgoingMessage.h +++ b/SignalServiceKit/src/Messages/Interactions/TSOutgoingMessage.h @@ -108,7 +108,8 @@ typedef NS_ENUM(NSInteger, TSGroupMetaMessage) { messageBody:(nullable NSString *)body attachmentId:(nullable NSString *)attachmentId expiresInSeconds:(uint32_t)expiresInSeconds - quotedMessage:(nullable TSQuotedMessage *)quotedMessage; + quotedMessage:(nullable TSQuotedMessage *)quotedMessage + linkPreview:(nullable OWSLinkPreview *)linkPreview; + (instancetype)outgoingMessageInThread:(nullable TSThread *)thread groupMetaMessage:(TSGroupMetaMessage)groupMetaMessage diff --git a/SignalServiceKit/src/Messages/Interactions/TSOutgoingMessage.m b/SignalServiceKit/src/Messages/Interactions/TSOutgoingMessage.m index 085469b63..81e90e1ba 100644 --- a/SignalServiceKit/src/Messages/Interactions/TSOutgoingMessage.m +++ b/SignalServiceKit/src/Messages/Interactions/TSOutgoingMessage.m @@ -231,7 +231,8 @@ NSString *NSStringForOutgoingMessageRecipientState(OWSOutgoingMessageRecipientSt messageBody:body attachmentId:attachmentId expiresInSeconds:0 - quotedMessage:nil]; + quotedMessage:nil + linkPreview:nil]; } + (instancetype)outgoingMessageInThread:(nullable TSThread *)thread @@ -243,7 +244,8 @@ NSString *NSStringForOutgoingMessageRecipientState(OWSOutgoingMessageRecipientSt messageBody:body attachmentId:attachmentId expiresInSeconds:expiresInSeconds - quotedMessage:nil]; + quotedMessage:nil + linkPreview:nil]; } + (instancetype)outgoingMessageInThread:(nullable TSThread *)thread @@ -251,6 +253,7 @@ NSString *NSStringForOutgoingMessageRecipientState(OWSOutgoingMessageRecipientSt attachmentId:(nullable NSString *)attachmentId expiresInSeconds:(uint32_t)expiresInSeconds quotedMessage:(nullable TSQuotedMessage *)quotedMessage + linkPreview:(nullable OWSLinkPreview *)linkPreview { NSMutableArray *attachmentIds = [NSMutableArray new]; if (attachmentId) { @@ -268,7 +271,7 @@ NSString *NSStringForOutgoingMessageRecipientState(OWSOutgoingMessageRecipientSt groupMetaMessage:TSGroupMetaMessageUnspecified quotedMessage:quotedMessage contactShare:nil - linkPreview:nil]; + linkPreview:linkPreview]; } + (instancetype)outgoingMessageInThread:(nullable TSThread *)thread @@ -964,6 +967,32 @@ NSString *NSStringForOutgoingMessageRecipientState(OWSOutgoingMessageRecipientSt } } + // Link Preview + if (self.linkPreview) { + SSKProtoDataMessagePreviewBuilder *previewBuilder = + [SSKProtoDataMessagePreview builderWithUrl:self.linkPreview.urlString]; + if (self.linkPreview.title.length > 0) { + [previewBuilder setTitle:self.linkPreview.title]; + } + if (self.linkPreview.imageAttachmentId) { + SSKProtoAttachmentPointer *_Nullable attachmentProto = + [TSAttachmentStream buildProtoForAttachmentId:self.linkPreview.imageAttachmentId]; + if (!attachmentProto) { + OWSFailDebug(@"Could not build link preview image protobuf."); + } else { + [previewBuilder setImage:attachmentProto]; + } + } + + NSError *error; + SSKProtoDataMessagePreview *_Nullable previewProto = [previewBuilder buildAndReturnError:&error]; + if (error || !previewProto) { + OWSFailDebug(@"Could not build link preview protobuf: %@.", error); + } else { + [builder setPreview:previewProto]; + } + } + return builder; } diff --git a/SignalServiceKit/src/Messages/OWSMessageSender.m b/SignalServiceKit/src/Messages/OWSMessageSender.m index 977f1cc4c..7e4b32aaa 100644 --- a/SignalServiceKit/src/Messages/OWSMessageSender.m +++ b/SignalServiceKit/src/Messages/OWSMessageSender.m @@ -354,6 +354,66 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException"; OWSAssertDebug([message.body lengthOfBytesUsingEncoding:NSUTF8StringEncoding] <= kOversizeTextMessageSizeThreshold); } + [self ensureLinkPreviewForMessage:message + completion:^{ + [self prepareMessageAndEnqueue:message success:successHandler failure:failureHandler]; + }]; +} + +- (void)ensureLinkPreviewForMessage:(TSOutgoingMessage *)message completion:(dispatch_block_t)completion +{ + OWSAssert(message); + OWSAssert(completion); + + if (message.linkPreview != nil) { + // Message already has link preview. + completion(); + return; + } + + [OWSLinkPreview + tryToBuildPreviewInfoForMessageBodyText:message.body + completion:^(OWSLinkPreviewInfo *_Nullable linkPreviewInfo) { + if (!linkPreviewInfo) { + completion(); + return; + } + + [self.dbConnection + asyncReadWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { + NSError *linkPreviewError; + OWSLinkPreview *_Nullable linkPreview = [OWSLinkPreview + buildValidatedLinkPreviewFromInfo:linkPreviewInfo + transaction:transaction + error:&linkPreviewError]; + if (linkPreviewError + && ![OWSLinkPreview isNoPreviewError:linkPreviewError]) { + OWSFailDebug(@"linkPreviewError: %@", linkPreviewError); + completion(); + return; + } + if (!linkPreview) { + OWSFailDebug(@"Missing linkPreview."); + completion(); + return; + } + + [message updateWithLinkPreview:linkPreview transaction:transaction]; + completion(); + }]; + }]; +} + +- (void)prepareMessageAndEnqueue:(TSOutgoingMessage *)message + success:(void (^)(void))successHandler + failure:(void (^)(NSError *error))failureHandler +{ + OWSAssertDebug(message); + if (message.body.length > 0) { + OWSAssertDebug( + [message.body lengthOfBytesUsingEncoding:NSUTF8StringEncoding] <= kOversizeTextMessageSizeThreshold); + } + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ __block NSArray *quotedThumbnailAttachments = @[]; diff --git a/SignalServiceKit/src/Network/ReverseProxy.swift b/SignalServiceKit/src/Network/ReverseProxy.swift new file mode 100644 index 000000000..275c614d7 --- /dev/null +++ b/SignalServiceKit/src/Network/ReverseProxy.swift @@ -0,0 +1,54 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +import Foundation + +@objc +public class ReverseProxy: NSObject { + + @available(*, unavailable, message:"do not instantiate this class.") + private override init() { + } + + @objc + public class func sessionConfiguration() -> URLSessionConfiguration { + let configuration = URLSessionConfiguration.ephemeral + let proxyHost = "contentproxy.signal.org" + let proxyPort = 443 + configuration.connectionProxyDictionary = [ + "HTTPEnable": 1, + "HTTPProxy": proxyHost, + "HTTPPort": proxyPort, + "HTTPSEnable": 1, + "HTTPSProxy": proxyHost, + "HTTPSPort": proxyPort + ] + return configuration + } + + @objc + public class func sessionManager(baseUrl baseUrlString: String?) -> AFHTTPSessionManager? { + guard let baseUrlString = baseUrlString else { + return AFHTTPSessionManager(baseURL: nil, sessionConfiguration: sessionConfiguration()) + } + guard let baseUrl = URL(string: baseUrlString) else { + owsFailDebug("Invalid base URL.") + return nil + } + let sessionManager = AFHTTPSessionManager(baseURL: baseUrl, + sessionConfiguration: sessionConfiguration()) + return sessionManager + } + + @objc + public class func jsonSessionManager(baseUrl: String) -> AFHTTPSessionManager? { + guard let sessionManager = self.sessionManager(baseUrl: baseUrl) else { + owsFailDebug("Could not create session manager") + return nil + } + sessionManager.requestSerializer = AFJSONRequestSerializer() + sessionManager.responseSerializer = AFJSONResponseSerializer() + return sessionManager + } +} From d775a70a8957c78334f89546adb3e20afb700599 Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Tue, 15 Jan 2019 15:55:42 -0500 Subject: [PATCH 2/5] Build link previews. --- SignalMessaging/utils/ThreadUtil.m | 91 ------------------- .../Interactions/OWSLinkPreview.swift | 14 +-- .../src/Messages/Interactions/TSMessage.h | 2 - .../src/Messages/Interactions/TSMessage.m | 13 --- .../src/Messages/OWSMessageSender.m | 60 ------------ 5 files changed, 4 insertions(+), 176 deletions(-) diff --git a/SignalMessaging/utils/ThreadUtil.m b/SignalMessaging/utils/ThreadUtil.m index 7d46bc6dd..28352351c 100644 --- a/SignalMessaging/utils/ThreadUtil.m +++ b/SignalMessaging/utils/ThreadUtil.m @@ -66,97 +66,6 @@ NS_ASSUME_NONNULL_BEGIN #pragma mark - Durable Message Enqueue -// TODO: Move this elsewhere. -+ (void)ensureLinkPreviewForMessage:(TSOutgoingMessage *)message completion:(dispatch_block_t)completion -{ - OWSAssert(message); - OWSAssert(completion); - - if (message.linkPreview != nil) { - // Message already has link preview. - completion(); - return; - } - - [OWSLinkPreview - tryToBuildPreviewInfoForMessageBodyText:message.body - completion:^(OWSLinkPreviewInfo *_Nullable linkPreviewInfo) { - if (!linkPreviewInfo) { - completion(); - return; - } - - [self.dbConnection - asyncReadWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { - NSError *linkPreviewError; - OWSLinkPreview *_Nullable linkPreview = [OWSLinkPreview - buildValidatedLinkPreviewFromInfo:linkPreviewInfo - transaction:transaction - error:&linkPreviewError]; - if (linkPreviewError - && ![OWSLinkPreview isNoPreviewError:linkPreviewError]) { - OWSFailDebug(@"linkPreviewError: %@", linkPreviewError); - completion(); - return; - } - if (!linkPreview) { - OWSFailDebug(@"Missing linkPreview."); - completion(); - return; - } - - [message updateWithLinkPreview:linkPreview transaction:transaction]; - completion(); - }]; - }]; -} - -//+ (TSOutgoingMessage *)enqueueMessageWithText:(NSString *)text -// inThread:(TSThread *)thread -// quotedReplyModel:(nullable OWSQuotedReplyModel *)quotedReplyModel -// linkPreview:(nullable OWSLinkPreview *)linkPreview -// transaction:(YapDatabaseReadTransaction *)transaction -//{ -// OWSDisappearingMessagesConfiguration *configuration = -// [OWSDisappearingMessagesConfiguration fetchObjectWithUniqueID:thread.uniqueId transaction:transaction]; -// -// uint32_t expiresInSeconds = (configuration.isEnabled ? configuration.durationSeconds : 0); -// -// TSOutgoingMessage *message = -// [TSOutgoingMessage outgoingMessageInThread:thread -// messageBody:text -// attachmentId:nil -// expiresInSeconds:expiresInSeconds -// quotedMessage:[quotedReplyModel buildQuotedMessageForSending] -// linkPreview:linkPreview]; -// -// [BenchManager benchAsyncWithTitle:@"Saving outgoing message" block:^(void (^benchmarkCompletion)(void)) { -// // To avoid blocking the send flow, we dispatch an async write from within this read transaction -// AnyPromise *promise = [[AnyPromise promiseWithResolverBlock:^(PMKResolver resolve) { -// [self.dbConnection asyncReadWriteWithBlock:^(YapDatabaseReadWriteTransaction *writeTransaction) { -// [message saveWithTransaction:writeTransaction]; -// } -// completionBlock:^{ -// resolve(@(1)); -// }]; -// }].thenInBackground(^{ -// return [AnyPromise promiseWithResolverBlock:^(PMKResolver resolve) { -// [ThreadUtil ensureLinkPreviewForMessage:message -// completion:^{ -// resolve(@(1)); -// }]; -// }]; -// }].thenInBackground(^{ -// [self.messageSenderJobQueue addMessage:message -// transaction:writeTransaction]; -// }].thenInBackground(^{ -// benchmarkCompletion(); -// }) retainUntilComplete]; -// }]; -// -// return message; -//} - + (TSOutgoingMessage *)enqueueMessageWithText:(NSString *)text inThread:(TSThread *)thread quotedReplyModel:(nullable OWSQuotedReplyModel *)quotedReplyModel diff --git a/SignalServiceKit/src/Messages/Interactions/OWSLinkPreview.swift b/SignalServiceKit/src/Messages/Interactions/OWSLinkPreview.swift index f6c806ab5..73106cc20 100644 --- a/SignalServiceKit/src/Messages/Interactions/OWSLinkPreview.swift +++ b/SignalServiceKit/src/Messages/Interactions/OWSLinkPreview.swift @@ -217,13 +217,17 @@ public class OWSLinkPreview: MTLModel { // MARK: - Domain Whitelist + // TODO: Finalize private static let linkDomainWhitelist = [ "youtube.com", "reddit.com", "imgur.com", + "instagram.com", + "giphy.com", "instagram.com" ] + // TODO: Finalize private static let mediaDomainWhitelist = [ "ytimg.com", "cdninstagram.com" @@ -233,16 +237,6 @@ public class OWSLinkPreview: MTLModel { "https" ] - // *.giphy.com - // *.youtube.com - // *.youtu.be - // *.ytimg.com - // *.reddit.com - // *.reddi.it - // *.imgur.com - // *.instagram.com - // *.cdninstagram.com - @objc public class func isValidLinkUrl(_ urlString: String) -> Bool { guard let url = URL(string: urlString) else { diff --git a/SignalServiceKit/src/Messages/Interactions/TSMessage.h b/SignalServiceKit/src/Messages/Interactions/TSMessage.h index 55297e154..cf353f3ca 100644 --- a/SignalServiceKit/src/Messages/Interactions/TSMessage.h +++ b/SignalServiceKit/src/Messages/Interactions/TSMessage.h @@ -61,8 +61,6 @@ NS_ASSUME_NONNULL_BEGIN - (void)updateWithExpireStartedAt:(uint64_t)expireStartedAt transaction:(YapDatabaseReadWriteTransaction *)transaction; -- (void)updateWithLinkPreview:(OWSLinkPreview *)linkPreview transaction:(YapDatabaseReadWriteTransaction *)transaction; - @end NS_ASSUME_NONNULL_END diff --git a/SignalServiceKit/src/Messages/Interactions/TSMessage.m b/SignalServiceKit/src/Messages/Interactions/TSMessage.m index 53def73d2..a1a6979b2 100644 --- a/SignalServiceKit/src/Messages/Interactions/TSMessage.m +++ b/SignalServiceKit/src/Messages/Interactions/TSMessage.m @@ -48,8 +48,6 @@ static const NSUInteger OWSMessageSchemaVersion = 4; */ @property (nonatomic, readonly) NSUInteger schemaVersion; -@property (nonatomic, nullable) OWSLinkPreview *linkPreview; - @end #pragma mark - @@ -421,17 +419,6 @@ static const NSUInteger OWSMessageSchemaVersion = 4; }]; } -- (void)updateWithLinkPreview:(OWSLinkPreview *)linkPreview transaction:(YapDatabaseReadWriteTransaction *)transaction -{ - OWSAssertDebug(linkPreview); - OWSAssertDebug(transaction); - - [self applyChangeToSelfAndLatestCopy:transaction - changeBlock:^(TSOutgoingMessage *message) { - message.linkPreview = linkPreview; - }]; -} - @end NS_ASSUME_NONNULL_END diff --git a/SignalServiceKit/src/Messages/OWSMessageSender.m b/SignalServiceKit/src/Messages/OWSMessageSender.m index 7e4b32aaa..977f1cc4c 100644 --- a/SignalServiceKit/src/Messages/OWSMessageSender.m +++ b/SignalServiceKit/src/Messages/OWSMessageSender.m @@ -354,66 +354,6 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException"; OWSAssertDebug([message.body lengthOfBytesUsingEncoding:NSUTF8StringEncoding] <= kOversizeTextMessageSizeThreshold); } - [self ensureLinkPreviewForMessage:message - completion:^{ - [self prepareMessageAndEnqueue:message success:successHandler failure:failureHandler]; - }]; -} - -- (void)ensureLinkPreviewForMessage:(TSOutgoingMessage *)message completion:(dispatch_block_t)completion -{ - OWSAssert(message); - OWSAssert(completion); - - if (message.linkPreview != nil) { - // Message already has link preview. - completion(); - return; - } - - [OWSLinkPreview - tryToBuildPreviewInfoForMessageBodyText:message.body - completion:^(OWSLinkPreviewInfo *_Nullable linkPreviewInfo) { - if (!linkPreviewInfo) { - completion(); - return; - } - - [self.dbConnection - asyncReadWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { - NSError *linkPreviewError; - OWSLinkPreview *_Nullable linkPreview = [OWSLinkPreview - buildValidatedLinkPreviewFromInfo:linkPreviewInfo - transaction:transaction - error:&linkPreviewError]; - if (linkPreviewError - && ![OWSLinkPreview isNoPreviewError:linkPreviewError]) { - OWSFailDebug(@"linkPreviewError: %@", linkPreviewError); - completion(); - return; - } - if (!linkPreview) { - OWSFailDebug(@"Missing linkPreview."); - completion(); - return; - } - - [message updateWithLinkPreview:linkPreview transaction:transaction]; - completion(); - }]; - }]; -} - -- (void)prepareMessageAndEnqueue:(TSOutgoingMessage *)message - success:(void (^)(void))successHandler - failure:(void (^)(NSError *error))failureHandler -{ - OWSAssertDebug(message); - if (message.body.length > 0) { - OWSAssertDebug( - [message.body lengthOfBytesUsingEncoding:NSUTF8StringEncoding] <= kOversizeTextMessageSizeThreshold); - } - dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ __block NSArray *quotedThumbnailAttachments = @[]; From 8e44bf554c3e409466dc6e571f714f522b586563 Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Thu, 17 Jan 2019 13:18:44 -0500 Subject: [PATCH 3/5] Respond to CR. --- Signal/src/network/GiphyAPI.swift | 2 +- Signal/src/network/GiphyDownloader.swift | 2 +- SignalMessaging/utils/ThreadUtil.m | 1 - .../src/Messages/Interactions/OWSLinkPreview.swift | 8 +++----- .../Network/{ReverseProxy.swift => ContentProxy.swift} | 2 +- 5 files changed, 6 insertions(+), 9 deletions(-) rename SignalServiceKit/src/Network/{ReverseProxy.swift => ContentProxy.swift} (97%) diff --git a/Signal/src/network/GiphyAPI.swift b/Signal/src/network/GiphyAPI.swift index 25121883c..181222191 100644 --- a/Signal/src/network/GiphyAPI.swift +++ b/Signal/src/network/GiphyAPI.swift @@ -281,7 +281,7 @@ extension GiphyError: LocalizedError { private let kGiphyBaseURL = "https://api.giphy.com/" private func giphyAPISessionManager() -> AFHTTPSessionManager? { - return ReverseProxy.jsonSessionManager(baseUrl: kGiphyBaseURL) + return ContentProxy.jsonSessionManager(baseUrl: kGiphyBaseURL) } // MARK: Search diff --git a/Signal/src/network/GiphyDownloader.swift b/Signal/src/network/GiphyDownloader.swift index 86c94ae91..71c329957 100644 --- a/Signal/src/network/GiphyDownloader.swift +++ b/Signal/src/network/GiphyDownloader.swift @@ -415,7 +415,7 @@ extension URLSessionTask { private lazy var giphyDownloadSession: URLSession = { AssertIsOnMainThread() - let configuration = ReverseProxy.sessionConfiguration() + let configuration = ContentProxy.sessionConfiguration() configuration.urlCache = nil configuration.requestCachePolicy = .reloadIgnoringCacheData configuration.httpMaximumConnectionsPerHost = 10 diff --git a/SignalMessaging/utils/ThreadUtil.m b/SignalMessaging/utils/ThreadUtil.m index 28352351c..0eb027186 100644 --- a/SignalMessaging/utils/ThreadUtil.m +++ b/SignalMessaging/utils/ThreadUtil.m @@ -8,7 +8,6 @@ #import "OWSQuotedReplyModel.h" #import "OWSUnreadIndicator.h" #import "TSUnreadIndicatorInteraction.h" -#import #import #import #import diff --git a/SignalServiceKit/src/Messages/Interactions/OWSLinkPreview.swift b/SignalServiceKit/src/Messages/Interactions/OWSLinkPreview.swift index 73106cc20..6c9fd5189 100644 --- a/SignalServiceKit/src/Messages/Interactions/OWSLinkPreview.swift +++ b/SignalServiceKit/src/Messages/Interactions/OWSLinkPreview.swift @@ -23,7 +23,6 @@ public class OWSLinkPreviewInfo: NSObject { @objc public var imageFilePath: String? - @objc public init(urlString: String, title: String?, imageFilePath: String? = nil) { self.urlString = urlString self.title = title @@ -152,7 +151,7 @@ public class OWSLinkPreview: MTLModel { guard OWSLinkPreview.featureEnabled else { throw LinkPreviewError.noPreview } - let imageAttachmentId = OWSLinkPreview.saveAttachmentIfPossible(forFilePath: info.imageFilePath, + let imageAttachmentId = OWSLinkPreview.saveAttachmentIfPossible(inputFilePath: info.imageFilePath, transaction: transaction) let linkPreview = OWSLinkPreview(urlString: info.urlString, title: info.title, imageAttachmentId: imageAttachmentId) @@ -165,7 +164,7 @@ public class OWSLinkPreview: MTLModel { return linkPreview } - private class func saveAttachmentIfPossible(forFilePath filePath: String?, + private class func saveAttachmentIfPossible(inputFilePath filePath: String?, transaction: YapDatabaseReadWriteTransaction) -> String? { guard let filePath = filePath else { return nil @@ -223,8 +222,7 @@ public class OWSLinkPreview: MTLModel { "reddit.com", "imgur.com", "instagram.com", - "giphy.com", - "instagram.com" + "giphy.com" ] // TODO: Finalize diff --git a/SignalServiceKit/src/Network/ReverseProxy.swift b/SignalServiceKit/src/Network/ContentProxy.swift similarity index 97% rename from SignalServiceKit/src/Network/ReverseProxy.swift rename to SignalServiceKit/src/Network/ContentProxy.swift index 275c614d7..098a9e557 100644 --- a/SignalServiceKit/src/Network/ReverseProxy.swift +++ b/SignalServiceKit/src/Network/ContentProxy.swift @@ -5,7 +5,7 @@ import Foundation @objc -public class ReverseProxy: NSObject { +public class ContentProxy: NSObject { @available(*, unavailable, message:"do not instantiate this class.") private override init() { From 6e044675ac5329957945acc845c7e851eeb2955f Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Thu, 17 Jan 2019 14:56:46 -0500 Subject: [PATCH 4/5] Respond to CR. --- .../Interactions/OWSLinkPreview.swift | 28 ++----------------- .../src/Util/NSRegularExpression+SSK.swift | 23 +++++++++++++++ 2 files changed, 26 insertions(+), 25 deletions(-) diff --git a/SignalServiceKit/src/Messages/Interactions/OWSLinkPreview.swift b/SignalServiceKit/src/Messages/Interactions/OWSLinkPreview.swift index 6c9fd5189..3dcb3ff34 100644 --- a/SignalServiceKit/src/Messages/Interactions/OWSLinkPreview.swift +++ b/SignalServiceKit/src/Messages/Interactions/OWSLinkPreview.swift @@ -389,7 +389,7 @@ public class OWSLinkPreview: MTLModel { Logger.verbose("url: \(url)") - guard let sessionManager: AFHTTPSessionManager = ReverseProxy.sessionManager(baseUrl: nil) else { + guard let sessionManager: AFHTTPSessionManager = ContentProxy.sessionManager(baseUrl: nil) else { owsFailDebug("Couldn't create session manager.") completion(nil) return @@ -456,10 +456,10 @@ public class OWSLinkPreview: MTLModel { } Logger.verbose("linkText: \(linkText)") - let title = parseFirstMatch(pattern: "", text: linkText) + let title = NSRegularExpression.parseFirstMatch(pattern: "", text: linkText) Logger.verbose("title: \(String(describing: title))") - guard let imageUrlString = parseFirstMatch(pattern: "", text: linkText) else { + guard let imageUrlString = NSRegularExpression.parseFirstMatch(pattern: "", text: linkText) else { return completion(OWSLinkPreviewInfo(urlString: linkUrlString, title: title)) } Logger.verbose("imageUrlString: \(imageUrlString)") @@ -510,26 +510,4 @@ public class OWSLinkPreview: MTLModel { completion(linkPreviewInfo) }) } - - private class func parseFirstMatch(pattern: String, - text: String) -> String? { - do { - let regex = try NSRegularExpression(pattern: pattern) - guard let match = regex.firstMatch(in: text, - options: [], - range: NSRange(location: 0, length: text.count)) else { - return nil - } - let matchRange = match.range(at: 1) - guard let textRange = Range(matchRange, in: text) else { - owsFailDebug("Invalid match.") - return nil - } - let substring = String(text[textRange]) - return substring - } catch { - Logger.error("Error: \(error)") - return nil - } - } } diff --git a/SignalServiceKit/src/Util/NSRegularExpression+SSK.swift b/SignalServiceKit/src/Util/NSRegularExpression+SSK.swift index 33d20d49d..b8e2fda49 100644 --- a/SignalServiceKit/src/Util/NSRegularExpression+SSK.swift +++ b/SignalServiceKit/src/Util/NSRegularExpression+SSK.swift @@ -11,4 +11,27 @@ public extension NSRegularExpression { public func hasMatch(input: String) -> Bool { return self.firstMatch(in: input, options: [], range: NSRange(location: 0, length: input.utf16.count)) != nil } + + @objc + public class func parseFirstMatch(pattern: String, + text: String) -> String? { + do { + let regex = try NSRegularExpression(pattern: pattern) + guard let match = regex.firstMatch(in: text, + options: [], + range: NSRange(location: 0, length: text.count)) else { + return nil + } + let matchRange = match.range(at: 1) + guard let textRange = Range(matchRange, in: text) else { + owsFailDebug("Invalid match.") + return nil + } + let substring = String(text[textRange]) + return substring + } catch { + Logger.error("Error: \(error)") + return nil + } + } } From e819f777d25daf35e1613ada90a26740515188ff Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Tue, 22 Jan 2019 10:12:34 -0500 Subject: [PATCH 5/5] Update Cocoapods. --- Pods | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Pods b/Pods index c65309265..8b2c886d3 160000 --- a/Pods +++ b/Pods @@ -1 +1 @@ -Subproject commit c653092655ec0c592d7e733e1bd5eee342849ae0 +Subproject commit 8b2c886d38cb286341dad31a83ba59a25c30879f