Merge pull request #51 from loki-project/gif

Direct GIF support
pull/52/head
gmbnt 6 years ago committed by GitHub
commit bb5eaea21e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -6,14 +6,10 @@
<dict>
<key>CarthageVersion</key>
<string>0.33.0</string>
<key>DateTime</key>
<string>Fri Sep 13 00:30:48 UTC 2019</string>
<key>OSXVersion</key>
<string>10.14.6</string>
<key>WebRTCCommit</key>
<string>1445d719bf05280270e9f77576f80f973fd847f8 M73</string>
<key>XCodeVersion</key>
<string>1000.1030</string>
</dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>

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

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

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

@ -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<OWSLinkPreview> {
// 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<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 +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).")

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

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

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

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

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

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

Loading…
Cancel
Save