Added incoming gif previews

This commit is contained in:
Mikunj 2019-09-16 14:45:24 +10:00
parent 54de8bc221
commit 815c8a97fb
8 changed files with 178 additions and 17 deletions

View File

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

View File

@ -290,6 +290,7 @@ NSUInteger const TSAttachmentSchemaVersion = 4;
#pragma mark - Relationships
- (nullable TSMessage *)fetchAlbumMessageWithTransaction:(YapDatabaseReadTransaction *)transaction
{
if (self.albumMessageId == nil) {

View File

@ -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,15 @@ 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,42 +219,46 @@ 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
}
attachment.save(with: transaction)
return attachment.uniqueId
}
private func isValid() -> Bool {
var hasTitle = false
if let titleValue = title {
@ -318,7 +338,12 @@ 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 +362,10 @@ public class OWSLinkPreview: MTLModel {
"fbcdn.net",
// Pinterest
"pinimg.com"
"pinimg.com",
// Giphy
"giphy.com",
]
private static let protocolWhitelist = [
@ -672,6 +700,66 @@ public class OWSLinkPreview: MTLModel {
})
return promise
}
public class func getImagePreview(fromUrl imageUrl: String, transaction: YapDatabaseReadWriteTransaction) -> Promise<OWSLinkPreview> {
// Get the mime types the url
guard let imageFileExtension = fileExtension(forImageUrl: imageUrl),
let imageMimeType = mimetype(forImageFileExtension: imageFileExtension) else {
return Promise(error: LinkPreviewError.invalidInput)
}
return downloadImage(url: imageUrl).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("Error: Failed to save attachment for \(imageUrl)")
throw LinkPreviewError.attachmentFailedToSave
}
// If we had 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: imageUrl, title: nil, imageAttachmentId: attachmentId)
}
return OWSLinkPreview(urlString: imageUrl, title: nil, imageAttachmentId: attachmentId, isDirectAttachment: true)
}
}
@objc(getImagePreviewFromUrl:transaction:)
public class func objc_getImagePreview(url imageUrl: String, transaction: YapDatabaseReadWriteTransaction) -> AnyPromise {
return AnyPromise.from(getImagePreview(fromUrl: imageUrl, transaction: transaction))
}
public class func downloadImage(url imageUrl: String) -> Promise<Data> {
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<Data> {
@ -710,6 +798,9 @@ public class OWSLinkPreview: MTLModel {
Logger.error("Could not parse image.")
return Promise(error: LinkPreviewError.invalidContent)
}
// If we have a gif then don't download it as a jpg and also we need to ensure that it's a valid GIF
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 +921,19 @@ 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 +945,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).")

View File

@ -67,6 +67,7 @@ typedef NS_ENUM(NSInteger, LKMessageFriendRequestStatus) {
- (NSArray<TSAttachment *> *)attachmentsWithTransaction:(YapDatabaseReadTransaction *)transaction;
- (NSArray<TSAttachment *> *)mediaAttachmentsWithTransaction:(YapDatabaseReadTransaction *)transaction;
- (nullable TSAttachment *)oversizeTextAttachmentWithTransaction:(YapDatabaseReadTransaction *)transaction;
- (void)addAttachmentId:(NSString *)attachmentId transaction:(YapDatabaseReadWriteTransaction *)transaction;
- (void)removeAttachment:(TSAttachment *)attachment
transaction:(YapDatabaseReadWriteTransaction *)transaction NS_SWIFT_NAME(removeAttachment(_:transaction:));

View File

@ -250,6 +250,12 @@ static const NSUInteger OWSMessageSchemaVersion = 4;
[self saveWithTransaction:transaction];
}
- (void)addAttachmentId:(NSString *)attachmentId transaction:(YapDatabaseReadWriteTransaction *)transaction {
if (!self.attachmentIds) { return; }
[self.attachmentIds addObject:attachmentId];
[self saveWithTransaction:transaction];
}
- (NSString *)debugDescription
{
if ([self hasAttachments] && self.body.length > 0) {

View File

@ -1525,6 +1525,38 @@ NS_ASSUME_NONNULL_BEGIN
incomingMessage.linkPreview = linkPreview;
[incomingMessage saveWithTransaction:transaction];
}];
})
.catchOn(dispatch_get_main_queue(), ^(NSError *error) {
// If we failed to get link preview due to invalid content then maybe it's a link to a direct image?
if ([OWSLinkPreview isInvalidContentError:error]) {
__block AnyPromise *promise;
[OWSPrimaryStorage.sharedManager.dbReadWriteConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
promise = [OWSLinkPreview getImagePreviewFromUrl:linkPreviewURL transaction:transaction];
}];
return promise;
}
// Return the error
return [AnyPromise promiseWithValue:error];
})
.thenOn(dispatch_get_main_queue(), ^(OWSLinkPreview *linkPreview) {
// If we managed to get direct previews then render them
[OWSPrimaryStorage.sharedManager.dbReadWriteConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
if (linkPreview.isDirectAttachment) {
if (!incomingMessage.hasAttachments) {
[incomingMessage addAttachmentId:linkPreview.imageAttachmentId transaction:transaction];
// Set the message id in attachment
TSAttachment *linkPreviewAttachment = [TSAttachment fetchObjectWithUniqueID:linkPreview.imageAttachmentId transaction:transaction];
linkPreviewAttachment.albumMessageId = incomingMessage.uniqueId;
[linkPreviewAttachment saveWithTransaction:transaction];
}
} else {
incomingMessage.linkPreview = linkPreview;
[incomingMessage saveWithTransaction:transaction];
}
}];
});
}
});

View File

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

View File

@ -263,6 +263,21 @@ 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++) {