Build link previews.

This commit is contained in:
Matthew Chen 2019-01-15 15:52:08 -05:00
parent d87fc27e78
commit 31ea64bdaf
14 changed files with 645 additions and 55 deletions

View file

@ -3983,6 +3983,7 @@ typedef enum : NSUInteger {
message = [ThreadUtil enqueueMessageWithText:text
inThread:self.thread
quotedReplyModel:self.inputToolbar.quotedReply
linkPreview:nil
transaction:transaction];
}];
}

View file

@ -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(), ^{

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

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

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

View file

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

View file

@ -8,6 +8,7 @@
#import "OWSQuotedReplyModel.h"
#import "OWSUnreadIndicator.h"
#import "TSUnreadIndicatorInteraction.h"
#import <PromiseKit/AnyPromise.h>
#import <SignalCoreKit/NSDate+OWS.h>
#import <SignalMessaging/OWSProfileManager.h>
#import <SignalMessaging/SignalMessaging-Swift.h>
@ -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];

View file

@ -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<AnyObject, AnyObject> = 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<AnyObject, OWSLinkPreviewInfo> = 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:
//
// <meta property="og:title" content="Randomness is Random - Numberphile">
// <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) {
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: "<meta property=\"og:title\" content=\"([^\"]+)\">", text: linkText)
Logger.verbose("title: \(String(describing: title))")
guard let imageUrlString = parseFirstMatch(pattern: "<meta property=\"og:image\" content=\"([^\"]+)\">", 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
}
}
}

View file

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

View file

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

View file

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

View file

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

View file

@ -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<TSAttachmentStream *> *quotedThumbnailAttachments = @[];

View file

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