diff --git a/Signal/Signal-Info.plist b/Signal/Signal-Info.plist
index b4ed0ec7c..96a81db1a 100644
--- a/Signal/Signal-Info.plist
+++ b/Signal/Signal-Info.plist
@@ -6,14 +6,10 @@
CarthageVersion
0.33.0
- DateTime
- Fri Sep 13 00:30:48 UTC 2019
OSXVersion
10.14.6
WebRTCCommit
1445d719bf05280270e9f77576f80f973fd847f8 M73
- XCodeVersion
- 1000.1030
CFBundleDevelopmentRegion
en
diff --git a/Signal/src/ViewControllers/ConversationView/ConversationInputToolbar.m b/Signal/src/ViewControllers/ConversationView/ConversationInputToolbar.m
index 39687b8c1..83044897c 100644
--- a/Signal/src/ViewControllers/ConversationView/ConversationInputToolbar.m
+++ b/Signal/src/ViewControllers/ConversationView/ConversationInputToolbar.m
@@ -962,16 +962,18 @@ const CGFloat kMaxTextViewHeight = 98;
// It's key that we use the *raw/unstripped* text, so we can reconcile cursor position with the
// selectedRange.
- NSString *_Nullable previewUrl = [OWSLinkPreview previewUrlForRawBodyText:self.inputTextView.text
- selectedRange:self.inputTextView.selectedRange];
+ NSString *_Nullable previewUrl = [OWSLinkPreview previewUrlForRawBodyText:self.inputTextView.text selectedRange:self.inputTextView.selectedRange];
+
+ if ([previewUrl hasSuffix:@".gif"]) {
+ return [self clearLinkPreviewStateAndView];
+ }
+
if (previewUrl.length < 1) {
- [self clearLinkPreviewStateAndView];
- return;
+ return [self clearLinkPreviewStateAndView];
}
if (self.inputLinkPreview && [self.inputLinkPreview.previewUrl isEqualToString:previewUrl]) {
- // No need to update.
- return;
+ return; // No need to update.
}
InputLinkPreview *inputLinkPreview = [InputLinkPreview new];
diff --git a/SignalServiceKit/src/Loki/API/LokiAPI.swift b/SignalServiceKit/src/Loki/API/LokiAPI.swift
index c81a39589..826a0e74b 100644
--- a/SignalServiceKit/src/Loki/API/LokiAPI.swift
+++ b/SignalServiceKit/src/Loki/API/LokiAPI.swift
@@ -4,7 +4,10 @@ import PromiseKit
public final class LokiAPI : NSObject {
internal static let storage = OWSPrimaryStorage.shared()
- internal static var userHexEncodedPublicKey: String { return OWSIdentityManager.shared().identityKeyPair()!.hexEncodedPublicKey }
+ // MARK: Convenience
+ internal static var userHexEncodedPublicKey: String {
+ return OWSIdentityManager.shared().identityKeyPair()!.hexEncodedPublicKey
+ }
// MARK: Settings
private static let version = "v1"
diff --git a/SignalServiceKit/src/Messages/Attachments/TSAttachment.h b/SignalServiceKit/src/Messages/Attachments/TSAttachment.h
index fea89979b..6b6e6b61c 100644
--- a/SignalServiceKit/src/Messages/Attachments/TSAttachment.h
+++ b/SignalServiceKit/src/Messages/Attachments/TSAttachment.h
@@ -43,7 +43,7 @@ typedef NS_ENUM(NSUInteger, TSAttachmentType) {
#pragma mark - Media Album
@property (nonatomic, readonly, nullable) NSString *caption;
-@property (nonatomic, readonly, nullable) NSString *albumMessageId;
+@property (nonatomic, nullable) NSString *albumMessageId;
- (nullable TSMessage *)fetchAlbumMessageWithTransaction:(YapDatabaseReadTransaction *)transaction;
// `migrateAlbumMessageId` is only used in the migration to the new multi-attachment message scheme,
diff --git a/SignalServiceKit/src/Messages/Interactions/OWSLinkPreview.swift b/SignalServiceKit/src/Messages/Interactions/OWSLinkPreview.swift
index 5c639d05a..c98a26d74 100644
--- a/SignalServiceKit/src/Messages/Interactions/OWSLinkPreview.swift
+++ b/SignalServiceKit/src/Messages/Interactions/OWSLinkPreview.swift
@@ -13,6 +13,8 @@ public enum LinkPreviewError: Int, Error {
case couldNotDownload
case featureDisabled
case invalidContent
+ case invalidMediaContent
+ case attachmentFailedToSave
}
// MARK: - OWSLinkPreviewDraft
@@ -81,12 +83,17 @@ public class OWSLinkPreview: MTLModel {
@objc
public var imageAttachmentId: String?
+
+ // Whether this preview can be rendered as an attachment
+ @objc
+ public var isDirectAttachment: Bool = false
@objc
- public init(urlString: String, title: String?, imageAttachmentId: String?) {
+ public init(urlString: String, title: String?, imageAttachmentId: String?, isDirectAttachment: Bool = false) {
self.urlString = urlString
self.title = title
self.imageAttachmentId = imageAttachmentId
+ self.isDirectAttachment = isDirectAttachment
super.init()
}
@@ -113,6 +120,12 @@ public class OWSLinkPreview: MTLModel {
}
return error == .noPreview
}
+
+ @objc
+ public class func isInvalidContentError(_ error: Error) -> Bool {
+ guard let error = error as? LinkPreviewError else { return false }
+ return error == .invalidContent
+ }
@objc
public class func buildValidatedLinkPreview(dataMessage: SSKProtoDataMessage,
@@ -203,33 +216,35 @@ public class OWSLinkPreview: MTLModel {
return linkPreview
}
-
+
private class func saveAttachmentIfPossible(jpegImageData: Data?,
transaction: YapDatabaseReadWriteTransaction) -> String? {
- guard let jpegImageData = jpegImageData else {
- return nil
- }
- let fileSize = jpegImageData.count
+ return saveAttachmentIfPossible(imageData: jpegImageData, mimeType: OWSMimeTypeImageJpeg, transaction: transaction);
+ }
+
+ private class func saveAttachmentIfPossible(imageData: Data?, mimeType: String, transaction: YapDatabaseReadWriteTransaction) -> String? {
+ guard let imageData = imageData else { return nil }
+
+ let fileSize = imageData.count
guard fileSize > 0 else {
owsFailDebug("Invalid file size for image data.")
return nil
}
- let fileExtension = "jpg"
- let contentType = OWSMimeTypeImageJpeg
-
+
+ guard let fileExtension = fileExtension(forMimeType: mimeType) else { return nil }
let filePath = OWSFileSystem.temporaryFilePath(withFileExtension: fileExtension)
do {
- try jpegImageData.write(to: NSURL.fileURL(withPath: filePath), options: .atomicWrite)
+ try imageData.write(to: NSURL.fileURL(withPath: filePath), options: .atomicWrite)
} catch let error as NSError {
owsFailDebug("file write failed: \(filePath), \(error)")
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: UInt32(fileSize), sourceFilename: nil, caption: nil, albumMessageId: nil)
+ let attachment = TSAttachmentStream(contentType: mimeType, byteCount: UInt32(fileSize), sourceFilename: nil, caption: nil, albumMessageId: nil)
guard attachment.write(dataSource) else {
owsFailDebug("Could not write data source for path: \(filePath)")
return nil
@@ -318,7 +333,13 @@ public class OWSLinkPreview: MTLModel {
// Pinterest
"pinterest.com",
"www.pinterest.com",
- "pin.it"
+ "pin.it",
+
+ // Giphy
+ "giphy.com",
+ "media.giphy.com",
+ "gph.is"
+
]
// For media domains, we DO NOT require an exact match - subdomains are allowed.
@@ -337,7 +358,10 @@ public class OWSLinkPreview: MTLModel {
"fbcdn.net",
// Pinterest
- "pinimg.com"
+ "pinimg.com",
+
+ // Giphy
+ "giphy.com"
]
private static let protocolWhitelist = [
@@ -672,6 +696,65 @@ public class OWSLinkPreview: MTLModel {
})
return promise
}
+
+ public class func getImagePreview(from url: String, in transaction: YapDatabaseReadWriteTransaction) -> Promise {
+ // Get the MIME type
+ guard let imageFileExtension = fileExtension(forImageUrl: url), let imageMIMEType = mimetype(forImageFileExtension: imageFileExtension) else {
+ return Promise(error: LinkPreviewError.invalidInput)
+ }
+
+ return downloadImage(url: url).map { data in
+ // Make sure the downloaded image has the correct MIME type
+ guard let newImageMIMEType = NSData(data: data).ows_guessMimeType() else {
+ throw LinkPreviewError.invalidContent
+ }
+
+ // Save the attachment
+ guard let attachmentId = saveAttachmentIfPossible(imageData: data, mimeType: newImageMIMEType, transaction: transaction) else {
+ Logger.verbose("Failed to save attachment for: \(url).")
+ throw LinkPreviewError.attachmentFailedToSave
+ }
+
+ // If it's a GIF and the data we have is not a GIF then we need to render a link preview without attachments
+ if (imageMIMEType == OWSMimeTypeImageGif && newImageMIMEType != OWSMimeTypeImageGif) {
+ return OWSLinkPreview(urlString: url, title: nil, imageAttachmentId: attachmentId)
+ }
+
+ return OWSLinkPreview(urlString: url, title: nil, imageAttachmentId: attachmentId, isDirectAttachment: true)
+ }
+ }
+
+ @objc(getImagePreviewFromUrl:transaction:)
+ public class func objc_getImagePreview(url: String, in transaction: YapDatabaseReadWriteTransaction) -> AnyPromise {
+ return AnyPromise.from(getImagePreview(from: url, in: transaction))
+ }
+
+ public class func downloadImage(url imageUrl: String) -> Promise {
+ guard OWSLinkPreview.featureEnabled else {
+ return Promise(error: LinkPreviewError.featureDisabled)
+ }
+
+ guard SSKPreferences.areLinkPreviewsEnabled else {
+ return Promise(error: LinkPreviewError.featureDisabled)
+ }
+
+ guard isValidMediaUrl(imageUrl) else {
+ Logger.error("Invalid image URL.")
+ return Promise.init(error: LinkPreviewError.invalidInput)
+ }
+
+ guard let imageFileExtension = fileExtension(forImageUrl: imageUrl) else {
+ Logger.error("Image URL has unknown or invalid file extension: \(imageUrl).")
+ return Promise.init(error: LinkPreviewError.invalidInput)
+ }
+
+ guard let imageMimeType = mimetype(forImageFileExtension: imageFileExtension) else {
+ Logger.error("Image URL has unknown or invalid content type: \(imageUrl).")
+ return Promise.init(error: LinkPreviewError.invalidInput)
+ }
+
+ return downloadImage(url: imageUrl, imageMimeType: imageMimeType)
+ }
private class func downloadImage(url urlString: String, imageMimeType: String) -> Promise {
@@ -710,6 +793,9 @@ public class OWSLinkPreview: MTLModel {
Logger.error("Could not parse image.")
return Promise(error: LinkPreviewError.invalidContent)
}
+
+ // Loki: If it's a GIF then ensure its validity and don't download it as a JPG
+ if (imageMimeType == OWSMimeTypeImageGif && NSData(data: data).ows_isValidImage(withMimeType: OWSMimeTypeImageGif)) { return Promise.value(data) }
let maxImageSize: CGFloat = 1024
let shouldResize = imageSize.width > maxImageSize || imageSize.height > maxImageSize
@@ -830,6 +916,15 @@ public class OWSLinkPreview: MTLModel {
}
return imageFileExtension
}
+
+ class func fileExtension(forMimeType mimeType: String) -> String? {
+ switch mimeType {
+ case OWSMimeTypeImageGif: return "gif"
+ case OWSMimeTypeImagePng: return "png"
+ case OWSMimeTypeImageJpeg: return "jpg"
+ default: return nil
+ }
+ }
class func mimetype(forImageFileExtension imageFileExtension: String) -> String? {
guard imageFileExtension.count > 0 else {
@@ -841,7 +936,8 @@ public class OWSLinkPreview: MTLModel {
}
let kValidMimeTypes = [
OWSMimeTypeImagePng,
- OWSMimeTypeImageJpeg
+ OWSMimeTypeImageJpeg,
+ OWSMimeTypeImageGif,
]
guard kValidMimeTypes.contains(imageMimeType) else {
Logger.error("Image URL has invalid content type: \(imageMimeType).")
diff --git a/SignalServiceKit/src/Messages/Interactions/TSMessage.h b/SignalServiceKit/src/Messages/Interactions/TSMessage.h
index 2488fd502..d115a4fe0 100644
--- a/SignalServiceKit/src/Messages/Interactions/TSMessage.h
+++ b/SignalServiceKit/src/Messages/Interactions/TSMessage.h
@@ -67,6 +67,7 @@ typedef NS_ENUM(NSInteger, LKMessageFriendRequestStatus) {
- (NSArray *)attachmentsWithTransaction:(YapDatabaseReadTransaction *)transaction;
- (NSArray *)mediaAttachmentsWithTransaction:(YapDatabaseReadTransaction *)transaction;
- (nullable TSAttachment *)oversizeTextAttachmentWithTransaction:(YapDatabaseReadTransaction *)transaction;
+- (void)addAttachmentWithID:(NSString *)attachmentID in:(YapDatabaseReadWriteTransaction *)transaction;
- (void)removeAttachment:(TSAttachment *)attachment
transaction:(YapDatabaseReadWriteTransaction *)transaction NS_SWIFT_NAME(removeAttachment(_:transaction:));
@@ -97,6 +98,10 @@ typedef NS_ENUM(NSInteger, LKMessageFriendRequestStatus) {
- (void)saveGroupChatMessageID:(uint64_t)serverMessageID in:(YapDatabaseReadWriteTransaction *_Nullable)transaction;
+#pragma mark - Link Preview
+
+- (void)generateLinkPreviewIfNeededFromURL:(NSString *)url;
+
@end
NS_ASSUME_NONNULL_END
diff --git a/SignalServiceKit/src/Messages/Interactions/TSMessage.m b/SignalServiceKit/src/Messages/Interactions/TSMessage.m
index 66f7922e0..033d5a80d 100644
--- a/SignalServiceKit/src/Messages/Interactions/TSMessage.m
+++ b/SignalServiceKit/src/Messages/Interactions/TSMessage.m
@@ -250,6 +250,12 @@ static const NSUInteger OWSMessageSchemaVersion = 4;
[self saveWithTransaction:transaction];
}
+- (void)addAttachmentWithID:(NSString *)attachmentID in:(YapDatabaseReadWriteTransaction *)transaction {
+ if (!self.attachmentIds) { return; }
+ [self.attachmentIds addObject:attachmentID];
+ [self saveWithTransaction:transaction];
+}
+
- (NSString *)debugDescription
{
if ([self hasAttachments] && self.body.length > 0) {
@@ -506,6 +512,50 @@ static const NSUInteger OWSMessageSchemaVersion = 4;
}
}
+#pragma mark - Link Preview
+
+
+- (void)generateLinkPreviewIfNeededFromURL:(NSString *)url {
+ // If we already have a link preview or attachment then don't bother
+ if (self.linkPreview != nil || self.hasAttachments) { return; }
+ [OWSLinkPreview tryToBuildPreviewInfoObjcWithPreviewUrl:url]
+ .thenOn(dispatch_get_main_queue(), ^(OWSLinkPreviewDraft *linkPreviewDraft) {
+ [self.dbReadWriteConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
+ OWSLinkPreview *linkPreview = [OWSLinkPreview buildValidatedLinkPreviewFromInfo:linkPreviewDraft transaction:transaction error:nil];
+ self.linkPreview = linkPreview;
+ [self saveWithTransaction:transaction];
+ }];
+ })
+ .catchOn(dispatch_get_main_queue(), ^(NSError *error) {
+ // If we failed to get a link preview due to an invalid content type error then this could be a direct image link
+ if ([OWSLinkPreview isInvalidContentError:error]) {
+ __block AnyPromise *promise;
+ [self.dbReadWriteConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
+ promise = [OWSLinkPreview getImagePreviewFromUrl:url transaction:transaction];
+ }];
+ return promise;
+ }
+ return [AnyPromise promiseWithValue:error];
+ })
+ .thenOn(dispatch_get_main_queue(), ^(OWSLinkPreview *linkPreview) {
+ // If we managed to get a direct image preview then render it
+ [self.dbReadWriteConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
+ if (linkPreview.isDirectAttachment) {
+ if (!self.hasAttachments) {
+ [self addAttachmentWithID:linkPreview.imageAttachmentId in:transaction];
+ TSAttachmentStream *linkPreviewAttachment = [TSAttachmentStream fetchObjectWithUniqueID:linkPreview.imageAttachmentId transaction:transaction];
+ linkPreviewAttachment.albumMessageId = self.uniqueId;
+ linkPreviewAttachment.isUploaded = true;
+ [linkPreviewAttachment saveWithTransaction:transaction];
+ }
+ } else {
+ self.linkPreview = linkPreview;
+ [self saveWithTransaction:transaction];
+ }
+ }];
+ });
+}
+
@end
NS_ASSUME_NONNULL_END
diff --git a/SignalServiceKit/src/Messages/OWSMessageManager.m b/SignalServiceKit/src/Messages/OWSMessageManager.m
index 5d5bad36a..6c88dd9ab 100644
--- a/SignalServiceKit/src/Messages/OWSMessageManager.m
+++ b/SignalServiceKit/src/Messages/OWSMessageManager.m
@@ -1409,14 +1409,7 @@ NS_ASSUME_NONNULL_BEGIN
dispatch_async(dispatch_get_main_queue(), ^{
NSString *url = [OWSLinkPreview previewURLForRawBodyText:incomingMessage.body];
if (url != nil) {
- [OWSLinkPreview tryToBuildPreviewInfoObjcWithPreviewUrl:url]
- .thenOn(dispatch_get_main_queue(), ^(OWSLinkPreviewDraft *linkPreviewDraft) {
- [OWSPrimaryStorage.sharedManager.dbReadWriteConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
- OWSLinkPreview *linkPreview = [OWSLinkPreview buildValidatedLinkPreviewFromInfo:linkPreviewDraft transaction:transaction error:nil];
- incomingMessage.linkPreview = linkPreview;
- [incomingMessage saveWithTransaction:transaction];
- }];
- });
+ [incomingMessage generateLinkPreviewIfNeededFromURL:url];
}
});
@@ -1518,14 +1511,7 @@ NS_ASSUME_NONNULL_BEGIN
linkPreviewURL = [OWSLinkPreview previewURLForRawBodyText:incomingMessage.body];
}
if (linkPreviewURL != nil) {
- [OWSLinkPreview tryToBuildPreviewInfoObjcWithPreviewUrl:linkPreviewURL]
- .thenOn(dispatch_get_main_queue(), ^(OWSLinkPreviewDraft *linkPreviewDraft) {
- [OWSPrimaryStorage.sharedManager.dbReadWriteConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
- OWSLinkPreview *linkPreview = [OWSLinkPreview buildValidatedLinkPreviewFromInfo:linkPreviewDraft transaction:transaction error:nil];
- incomingMessage.linkPreview = linkPreview;
- [incomingMessage saveWithTransaction:transaction];
- }];
- });
+ [incomingMessage generateLinkPreviewIfNeededFromURL:linkPreviewURL];
}
});
diff --git a/SignalServiceKit/src/Messages/OWSMessageSender.m b/SignalServiceKit/src/Messages/OWSMessageSender.m
index ebde64588..3dbcd588f 100644
--- a/SignalServiceKit/src/Messages/OWSMessageSender.m
+++ b/SignalServiceKit/src/Messages/OWSMessageSender.m
@@ -1239,6 +1239,15 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
// have a valid Signal account.
[SignalRecipient markRecipientAsRegisteredAndGet:recipient.recipientId transaction:transaction];
}];
+
+ // Loki: Check if we need to generate a link preview
+ TSMessage *message = messageSend.message;
+ if (message.linkPreview == nil && !message.hasAttachments) {
+ dispatch_async(dispatch_get_main_queue(), ^{
+ NSString *url = [OWSLinkPreview previewURLForRawBodyText:message.body];
+ if (url) { [message generateLinkPreviewIfNeededFromURL:url]; }
+ });
+ }
messageSend.success();
});
diff --git a/SignalServiceKit/src/Util/NSData+Image.h b/SignalServiceKit/src/Util/NSData+Image.h
index 8ba02fff3..7c588fa03 100644
--- a/SignalServiceKit/src/Util/NSData+Image.h
+++ b/SignalServiceKit/src/Util/NSData+Image.h
@@ -12,6 +12,7 @@ NS_ASSUME_NONNULL_BEGIN
+ (BOOL)ows_isValidImageAtPath:(NSString *)filePath mimeType:(nullable NSString *)mimeType;
- (BOOL)ows_isValidImage;
- (BOOL)ows_isValidImageWithMimeType:(nullable NSString *)mimeType;
+- (NSString *_Nullable)ows_guessMimeType;
// Returns the image size in pixels.
//
diff --git a/SignalServiceKit/src/Util/NSData+Image.m b/SignalServiceKit/src/Util/NSData+Image.m
index 13610f8ab..1463758fa 100644
--- a/SignalServiceKit/src/Util/NSData+Image.m
+++ b/SignalServiceKit/src/Util/NSData+Image.m
@@ -263,6 +263,17 @@ typedef NS_ENUM(NSInteger, ImageFormat) {
return ImageFormat_Unknown;
}
+- (NSString *_Nullable)ows_guessMimeType
+{
+ ImageFormat format = [self ows_guessImageFormat];
+ switch (format) {
+ case ImageFormat_Gif: return OWSMimeTypeImageGif;
+ case ImageFormat_Png: return OWSMimeTypeImagePng;
+ case ImageFormat_Jpeg: return OWSMimeTypeImageJpeg;
+ default: return nil;
+ }
+}
+
+ (BOOL)ows_areByteArraysEqual:(NSUInteger)length left:(unsigned char *)left right:(unsigned char *)right
{
for (NSUInteger i = 0; i < length; i++) {