Signal-iOS/SignalServiceKit/Messages/Attachments/TSAttachmentPointer.m
2024-06-17 11:56:26 -07:00

376 lines
14 KiB
Objective-C

//
// Copyright 2017 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
#import "TSAttachmentPointer.h"
#import "TSAttachmentStream.h"
#import <SignalServiceKit/SignalServiceKit-Swift.h>
NS_ASSUME_NONNULL_BEGIN
NSString *NSStringForTSAttachmentPointerState(TSAttachmentPointerState value)
{
switch (value) {
case TSAttachmentPointerStateEnqueued:
return @"Enqueued";
case TSAttachmentPointerStateDownloading:
return @"Downloading";
case TSAttachmentPointerStateFailed:
return @"Failed";
case TSAttachmentPointerStatePendingMessageRequest:
return @"PendingMessageRequest";
case TSAttachmentPointerStatePendingManualDownload:
return @"PendingManualDownload";
default:
OWSCFailDebug(@"Invalid value.");
return @"Invalid value";
}
}
static const NSUInteger kMaxAttachmentsPerDataMessage = 100;
@interface TSAttachmentStream (TSAttachmentPointer)
- (CGSize)cachedMediaSize;
@end
#pragma mark -
@interface TSAttachmentPointer ()
@property (nonatomic) TSAttachmentPointerState state;
// Optional property. Only set for attachments which need "lazy backup restore."
@property (nonatomic, nullable) NSString *lazyRestoreFragmentId;
@end
#pragma mark -
@implementation TSAttachmentPointer
- (nullable instancetype)initWithCoder:(NSCoder *)coder
{
self = [super initWithCoder:coder];
if (!self) {
return self;
}
// A TSAttachmentPointer is a yet-to-be-downloaded attachment.
// If this is an old TSAttachmentPointer from another session,
// we know that it failed to complete before the session completed.
if (![coder containsValueForKey:@"state"]) {
_state = TSAttachmentPointerStateFailed;
}
// Put a meaningless value into the meaningless field
_pointerType = 0;
return self;
}
- (instancetype)initWithServerId:(UInt64)serverId
cdnKey:(NSString *)cdnKey
cdnNumber:(UInt32)cdnNumber
key:(NSData *)key
digest:(nullable NSData *)digest
byteCount:(UInt32)byteCount
contentType:(NSString *)contentType
clientUuid:(NSUUID *)clientUuid
sourceFilename:(nullable NSString *)sourceFilename
caption:(nullable NSString *)caption
albumMessageId:(nullable NSString *)albumMessageId
attachmentType:(TSAttachmentType)attachmentType
mediaSize:(CGSize)mediaSize
blurHash:(nullable NSString *)blurHash
uploadTimestamp:(unsigned long long)uploadTimestamp
videoDuration:(nullable NSNumber *)videoDuration
{
self = [super initWithServerId:serverId
cdnKey:cdnKey
cdnNumber:cdnNumber
encryptionKey:key
byteCount:byteCount
contentType:contentType
clientUuid:clientUuid
sourceFilename:sourceFilename
caption:caption
attachmentType:attachmentType
albumMessageId:albumMessageId
blurHash:blurHash
uploadTimestamp:uploadTimestamp
videoDuration:videoDuration];
if (!self) {
return self;
}
_digest = digest;
_state = TSAttachmentPointerStateEnqueued;
// Put a meaningless value into the meaningless field
_pointerType = 0;
_mediaSize = mediaSize;
return self;
}
// --- CODE GENERATION MARKER
// This snippet is generated by /Scripts/sds_codegen/sds_generate.py. Do not manually edit it, instead run
// `sds_codegen.sh`.
// clang-format off
- (instancetype)initWithGrdbId:(int64_t)grdbId
uniqueId:(NSString *)uniqueId
albumMessageId:(nullable NSString *)albumMessageId
attachmentSchemaVersion:(NSUInteger)attachmentSchemaVersion
attachmentType:(TSAttachmentType)attachmentType
blurHash:(nullable NSString *)blurHash
byteCount:(unsigned int)byteCount
caption:(nullable NSString *)caption
cdnKey:(NSString *)cdnKey
cdnNumber:(unsigned int)cdnNumber
clientUuid:(nullable NSString *)clientUuid
contentType:(NSString *)contentType
encryptionKey:(nullable NSData *)encryptionKey
serverId:(unsigned long long)serverId
sourceFilename:(nullable NSString *)sourceFilename
uploadTimestamp:(unsigned long long)uploadTimestamp
videoDuration:(nullable NSNumber *)videoDuration
digest:(nullable NSData *)digest
lazyRestoreFragmentId:(nullable NSString *)lazyRestoreFragmentId
mediaSize:(CGSize)mediaSize
pointerType:(NSUInteger)pointerType
state:(TSAttachmentPointerState)state
{
self = [super initWithGrdbId:grdbId
uniqueId:uniqueId
albumMessageId:albumMessageId
attachmentSchemaVersion:attachmentSchemaVersion
attachmentType:attachmentType
blurHash:blurHash
byteCount:byteCount
caption:caption
cdnKey:cdnKey
cdnNumber:cdnNumber
clientUuid:clientUuid
contentType:contentType
encryptionKey:encryptionKey
serverId:serverId
sourceFilename:sourceFilename
uploadTimestamp:uploadTimestamp
videoDuration:videoDuration];
if (!self) {
return self;
}
_digest = digest;
_lazyRestoreFragmentId = lazyRestoreFragmentId;
_mediaSize = mediaSize;
_pointerType = pointerType;
_state = state;
[self sdsFinalizeAttachmentPointer];
return self;
}
// clang-format on
// --- CODE GENERATION MARKER
- (void)sdsFinalizeAttachmentPointer
{
[self upgradeAttachmentSchemaVersionIfNecessary];
}
+ (nullable TSAttachmentPointer *)attachmentPointerFromProto:(SSKProtoAttachmentPointer *)attachmentProto
albumMessage:(nullable TSMessage *)albumMessage
{
// TODO: Use the cdnKey and cdnNumber to fetch the attachment
if (attachmentProto.cdnID < 1 && attachmentProto.cdnKey.length < 1) {
OWSFailDebug(@"Invalid attachment id.");
return nil;
}
if (attachmentProto.key.length < 1) {
OWSFailDebug(@"Invalid attachment key.");
return nil;
}
NSUUID *_Nullable clientUuid;
if (attachmentProto.hasClientUuid) {
clientUuid = [NSUUID fromData:attachmentProto.clientUuid];
} else {
clientUuid = nil;
}
NSString *_Nullable fileName = attachmentProto.fileName;
NSString *_Nullable contentType = attachmentProto.contentType;
if (contentType.length < 1) {
// Content type might not set if the sending client can't
// infer a MIME type from the file extension.
OWSLogWarn(@"Invalid attachment content type.");
NSString *_Nullable fileExtension = [fileName pathExtension].lowercaseString;
if (fileExtension.length > 0) {
contentType = [MimeTypeUtil mimeTypeForFileExtension:fileExtension];
}
if (contentType.length < 1) {
contentType = MimeTypeUtil.mimeTypeApplicationOctetStream;
}
}
// digest will be empty for old clients.
NSData *_Nullable digest = attachmentProto.hasDigest ? attachmentProto.digest : nil;
TSAttachmentType attachmentType = TSAttachmentTypeDefault;
if ([attachmentProto hasFlags]) {
UInt32 flags = attachmentProto.flags;
if ((flags & SSKProtoAttachmentPointerFlagsVoiceMessage) > 0) {
attachmentType = TSAttachmentTypeVoiceMessage;
} else if ((flags & SSKProtoAttachmentPointerFlagsBorderless) > 0) {
attachmentType = TSAttachmentTypeBorderless;
} else if ((flags & SSKProtoAttachmentPointerFlagsGif) > 0) {
attachmentType = TSAttachmentTypeGIF;
}
}
NSString *_Nullable caption;
if (attachmentProto.hasCaption) {
caption = attachmentProto.caption;
}
NSString *_Nullable albumMessageId;
if (albumMessage != nil) {
albumMessageId = albumMessage.uniqueId;
}
CGSize mediaSize = CGSizeZero;
if (attachmentProto.hasWidth && attachmentProto.hasHeight && attachmentProto.width > 0
&& attachmentProto.height > 0) {
mediaSize = CGSizeMake(attachmentProto.width, attachmentProto.height);
}
UInt64 serverId = attachmentProto.cdnID;
if (![SDS fitsInInt64:serverId]) {
OWSFailDebug(@"Invalid server id.");
return nil;
}
NSString *_Nullable blurHash;
if (contentType.length > 0 && [MimeTypeUtil isSupportedVisualMediaMimeType:contentType]
&& attachmentProto.hasBlurHash) {
blurHash = attachmentProto.blurHash;
if (![BlurHash isValidBlurHash:blurHash]) {
blurHash = nil;
}
}
unsigned long long uploadTimestamp = 0;
if (attachmentProto.hasUploadTimestamp) {
uploadTimestamp = attachmentProto.uploadTimestamp;
}
TSAttachmentPointer *pointer = [[TSAttachmentPointer alloc] initWithServerId:serverId
cdnKey:attachmentProto.cdnKey
cdnNumber:attachmentProto.cdnNumber
key:attachmentProto.key
digest:digest
byteCount:attachmentProto.size
contentType:contentType
clientUuid:clientUuid
sourceFilename:fileName
caption:caption
albumMessageId:albumMessageId
attachmentType:attachmentType
mediaSize:mediaSize
blurHash:blurHash
uploadTimestamp:uploadTimestamp
videoDuration:nil];
return pointer;
}
+ (NSArray<TSAttachmentPointer *> *)attachmentPointersFromProtos:
(NSArray<SSKProtoAttachmentPointer *> *)attachmentProtos
albumMessage:(TSMessage *)albumMessage
{
OWSAssertDebug(attachmentProtos);
OWSAssertDebug(albumMessage);
NSUInteger pointerCount = MIN(kMaxAttachmentsPerDataMessage, attachmentProtos.count);
NSMutableArray *attachmentPointers = [[NSMutableArray alloc] initWithCapacity:pointerCount];
for (NSUInteger i = 0; i < pointerCount; i = i + 1) {
SSKProtoAttachmentPointer *attachmentProto = attachmentProtos[i];
TSAttachmentPointer *_Nullable attachmentPointer = [self attachmentPointerFromProto:attachmentProto
albumMessage:albumMessage];
if (attachmentPointer) {
[attachmentPointers addObject:attachmentPointer];
}
}
return [attachmentPointers copy];
}
#pragma mark - Update With... Methods
- (void)anyWillInsertWithTransaction:(SDSAnyWriteTransaction *)transaction
{
[super anyWillInsertWithTransaction:transaction];
[self checkForStreamOverwrite:transaction];
}
- (void)anyWillUpdateWithTransaction:(SDSAnyWriteTransaction *)transaction
{
[super anyWillUpdateWithTransaction:transaction];
[self checkForStreamOverwrite:transaction];
}
- (void)checkForStreamOverwrite:(SDSAnyReadTransaction *)transaction
{
#ifdef DEBUG
if (self.uniqueId.length > 0) {
TSAttachment *_Nullable oldObject = [TSAttachment anyFetchWithUniqueId:self.uniqueId transaction:transaction];
if ([oldObject isKindOfClass:[TSAttachmentStream class]]) {
OWSFailDebug(@"We should never overwrite a TSAttachmentStream with a TSAttachmentPointer.");
}
} else {
OWSFailDebug(@"Missing uniqueId.");
}
#endif
}
#if TESTABLE_BUILD
- (void)setAttachmentPointerStateDebug:(TSAttachmentPointerState)state
{
self.state = state;
}
#endif
- (void)updateWithAttachmentPointerState:(TSAttachmentPointerState)state
transaction:(SDSAnyWriteTransaction *)transaction
{
[self anyUpdateAttachmentPointerWithTransaction:transaction
block:^(TSAttachmentPointer *attachmentPointer) {
attachmentPointer.state = state;
}];
}
- (void)updateAttachmentPointerStateFrom:(TSAttachmentPointerState)oldState
to:(TSAttachmentPointerState)newState
transaction:(SDSAnyWriteTransaction *)transaction
{
[self anyUpdateAttachmentPointerWithTransaction:transaction
block:^(TSAttachmentPointer *attachmentPointer) {
if (attachmentPointer.state == oldState) {
attachmentPointer.state = newState;
}
}];
}
@end
NS_ASSUME_NONNULL_END