diff --git a/SignalServiceKit/MessageBackup/Archivers/ChatItem/MessageBackupMessageAttachmentArchiver.swift b/SignalServiceKit/MessageBackup/Archivers/ChatItem/MessageBackupMessageAttachmentArchiver.swift index 18328cc821..2a2d7d42db 100644 --- a/SignalServiceKit/MessageBackup/Archivers/ChatItem/MessageBackupMessageAttachmentArchiver.swift +++ b/SignalServiceKit/MessageBackup/Archivers/ChatItem/MessageBackupMessageAttachmentArchiver.swift @@ -86,6 +86,45 @@ internal class MessageBackupMessageAttachmentArchiver: MessageBackupProtoArchive ) } + public func restoreQuotedReplyThumbnailAttachment( + _ attachment: BackupProto_MessageAttachment, + chatItemId: MessageBackup.ChatItemId, + messageRowId: Int64, + message: TSMessage, + thread: MessageBackup.ChatThread, + tx: DBWriteTransaction + ) -> MessageBackup.RestoreInteractionResult { + let clientUUID: UUID? + if attachment.hasClientUuid { + guard let uuid = UUID(data: attachment.clientUuid) else { + return .messageFailure([.restoreFrameError( + .invalidProtoData(.invalidAttachmentClientUUID), + chatItemId + )]) + } + clientUUID = uuid + } else { + clientUUID = nil + } + + let ownedAttachment = OwnedAttachmentBackupPointerProto( + proto: attachment.pointer, + renderingFlag: attachment.flag.asAttachmentFlag, + clientUUID: clientUUID, + owner: .quotedReplyAttachment(.init( + messageRowId: messageRowId, + receivedAtTimestamp: message.receivedAtTimestamp, + threadRowId: thread.threadRowId + )) + ) + + return restoreAttachments( + [ownedAttachment], + chatItemId: chatItemId, + tx: tx + ) + } + private func restoreAttachments( _ attachments: [OwnedAttachmentBackupPointerProto], chatItemId: MessageBackup.ChatItemId, diff --git a/SignalServiceKit/MessageBackup/Archivers/ChatItem/MessageBackupTSMessageContentsArchiver.swift b/SignalServiceKit/MessageBackup/Archivers/ChatItem/MessageBackupTSMessageContentsArchiver.swift index 3bc52917b8..5dfbaf37e1 100644 --- a/SignalServiceKit/MessageBackup/Archivers/ChatItem/MessageBackupTSMessageContentsArchiver.swift +++ b/SignalServiceKit/MessageBackup/Archivers/ChatItem/MessageBackupTSMessageContentsArchiver.swift @@ -30,6 +30,7 @@ extension MessageBackup { fileprivate let reactions: [BackupProto_Reaction] fileprivate let bodyAttachments: [BackupProto_MessageAttachment] + fileprivate let quotedMessageThumbnail: BackupProto_MessageAttachment? } struct Payment { @@ -395,39 +396,47 @@ class MessageBackupTSMessageContentsArchiver: MessageBackupProtoArchiver { )]) } - let restoreInteractionResult: RestoreInteractionResult - let restoreBodyAttachmentsResult: RestoreInteractionResult + var downstreamObjectResults = [RestoreInteractionResult]() switch restoredContents { case .text(let text): - restoreInteractionResult = reactionArchiver.restoreReactions( + downstreamObjectResults.append(reactionArchiver.restoreReactions( text.reactions, chatItemId: chatItemId, message: message, context: context.recipientContext, tx: tx - ) - restoreBodyAttachmentsResult = attachmentsArchiver.restoreBodyAttachments( + )) + downstreamObjectResults.append(attachmentsArchiver.restoreBodyAttachments( text.bodyAttachments, chatItemId: chatItemId, messageRowId: messageRowId, message: message, thread: thread, tx: tx - ) + )) + if let quotedMessageThumbnail = text.quotedMessageThumbnail { + downstreamObjectResults.append(attachmentsArchiver.restoreQuotedReplyThumbnailAttachment( + quotedMessageThumbnail, + chatItemId: chatItemId, + messageRowId: messageRowId, + message: message, + thread: thread, + tx: tx + )) + } case .archivedPayment(let archivedPayment): - restoreInteractionResult = restoreArchivedPaymentContents( + downstreamObjectResults.append(restoreArchivedPaymentContents( archivedPayment, chatItemId: chatItemId, thread: thread, message: message, tx: tx - ) - // No body attachments to restore - restoreBodyAttachmentsResult = .success(()) + )) } - return restoreInteractionResult - .combine(restoreBodyAttachmentsResult) + return downstreamObjectResults.reduce(.success(()), { + $0.combine($1) + }) } // MARK: Helpers @@ -524,6 +533,7 @@ class MessageBackupTSMessageContentsArchiver: MessageBackupProtoArchiver { var partialErrors = [RestoreFrameError]() let quotedMessage: TSQuotedMessage? + let quotedMessageThumbnail: BackupProto_MessageAttachment? if standardMessage.hasQuote { guard let quoteResult = restoreQuote( @@ -536,9 +546,10 @@ class MessageBackupTSMessageContentsArchiver: MessageBackupProtoArchiver { else { return .messageFailure(partialErrors) } - quotedMessage = quoteResult + (quotedMessage, quotedMessageThumbnail) = quoteResult } else { quotedMessage = nil + quotedMessageThumbnail = nil } guard standardMessage.hasText else { @@ -554,7 +565,8 @@ class MessageBackupTSMessageContentsArchiver: MessageBackupProtoArchiver { body: body, quotedMessage: quotedMessage, reactions: standardMessage.reactions, - bodyAttachments: standardMessage.attachments + bodyAttachments: standardMessage.attachments, + quotedMessageThumbnail: quotedMessageThumbnail ))) case .partialRestore(let body, let partialErrors): return .partialRestore( @@ -562,7 +574,8 @@ class MessageBackupTSMessageContentsArchiver: MessageBackupProtoArchiver { body: body, quotedMessage: quotedMessage, reactions: standardMessage.reactions, - bodyAttachments: standardMessage.attachments + bodyAttachments: standardMessage.attachments, + quotedMessageThumbnail: quotedMessageThumbnail )), partialErrors ) @@ -655,7 +668,7 @@ class MessageBackupTSMessageContentsArchiver: MessageBackupProtoArchiver { thread: MessageBackup.ChatThread, context: MessageBackup.ChatRestoringContext, tx: DBReadTransaction - ) -> RestoreInteractionResult { + ) -> RestoreInteractionResult<(TSQuotedMessage, BackupProto_MessageAttachment?)> { let authorAddress: MessageBackup.InteropAddress switch context.recipientContext[quote.authorRecipientId] { case .none: @@ -735,26 +748,48 @@ class MessageBackupTSMessageContentsArchiver: MessageBackupProtoArchiver { isGiftBadge = true } - guard let quoteBody else { - // Non-text not supported yet. - return .messageFailure([.restoreFrameError(.unimplemented, chatItemId)]) + let quotedAttachmentInfo: OWSAttachmentInfo? + let quotedAttachmentThumbnail: BackupProto_MessageAttachment? + if let quotedAttachmentProto = quote.attachments.first { + if quotedAttachmentProto.hasThumbnail { + quotedAttachmentInfo = .init(forV2ThumbnailReference: ()) + quotedAttachmentThumbnail = quotedAttachmentProto.thumbnail + } else { + let mimeType = quotedAttachmentProto.contentType.nilIfEmpty + ?? MimeType.applicationOctetStream.rawValue + let sourceFilename = quotedAttachmentProto.fileName.nilIfEmpty + quotedAttachmentInfo = .init( + stubWithMimeType: mimeType, + sourceFilename: sourceFilename + ) + quotedAttachmentThumbnail = nil + } + } else { + quotedAttachmentInfo = nil + quotedAttachmentThumbnail = nil } - // TODO: [Backups] Support attachments + guard quoteBody != nil || quotedAttachmentThumbnail != nil || isGiftBadge else { + return .messageFailure([.restoreFrameError( + .invalidProtoData(.quotedMessageEmptyContent), + chatItemId + )]) + } let quotedMessage = TSQuotedMessage( targetMessageTimestamp: targetMessageTimestamp, authorAddress: authorAddress, - body: quoteBody.text, - bodyRanges: quoteBody.ranges, + body: quoteBody?.text, + bodyRanges: quoteBody?.ranges, bodySource: bodySource, + quotedAttachmentInfo: quotedAttachmentInfo, isGiftBadge: isGiftBadge ) if partialErrors.isEmpty { - return .success(quotedMessage) + return .success((quotedMessage, quotedAttachmentThumbnail)) } else { - return .partialRestore(quotedMessage, partialErrors) + return .partialRestore((quotedMessage, quotedAttachmentThumbnail), partialErrors) } } diff --git a/SignalServiceKit/MessageBackup/Archivers/MessageBackupErrors.swift b/SignalServiceKit/MessageBackup/Archivers/MessageBackupErrors.swift index fa7988137b..a986c8d903 100644 --- a/SignalServiceKit/MessageBackup/Archivers/MessageBackupErrors.swift +++ b/SignalServiceKit/MessageBackup/Archivers/MessageBackupErrors.swift @@ -364,6 +364,10 @@ extension MessageBackup { /// A `BackupProto_BodyRange` with a missing or unrecognized style. case unrecognizedBodyRangeStyle + /// A quoted message had no body, attachment, gift badge, or other + /// content in its representation of the original being quoted. + case quotedMessageEmptyContent + /// A `BackupProto_Group's` gv2 master key could not be parsed by libsignal. case invalidGV2MasterKey /// A `BackupProto_Group` was missing its group snapshot. @@ -594,6 +598,7 @@ extension MessageBackup { .unrecognizedMessageSendStatus, .reactionNotFromAciOrE164, .unrecognizedBodyRangeStyle, + .quotedMessageEmptyContent, .invalidGV2MasterKey, .missingGV2GroupSnapshot, .unrecognizedGV2MemberRole, diff --git a/SignalServiceKit/Messages/Interactions/Quotes/QuotedReplyManagerImpl.swift b/SignalServiceKit/Messages/Interactions/Quotes/QuotedReplyManagerImpl.swift index 36956fbbbe..96050831b3 100644 --- a/SignalServiceKit/Messages/Interactions/Quotes/QuotedReplyManagerImpl.swift +++ b/SignalServiceKit/Messages/Interactions/Quotes/QuotedReplyManagerImpl.swift @@ -740,6 +740,7 @@ public class QuotedReplyManagerImpl: QuotedReplyManager { ), bodyRanges: nil, bodySource: .remote, + quotedAttachmentInfo: nil, isGiftBadge: false )) } diff --git a/SignalServiceKit/Messages/Interactions/Quotes/TSQuotedMessage.h b/SignalServiceKit/Messages/Interactions/Quotes/TSQuotedMessage.h index 332d70288b..2631ca4238 100644 --- a/SignalServiceKit/Messages/Interactions/Quotes/TSQuotedMessage.h +++ b/SignalServiceKit/Messages/Interactions/Quotes/TSQuotedMessage.h @@ -119,12 +119,12 @@ typedef NS_ENUM(NSUInteger, OWSAttachmentInfoReference) { isGiftBadge:(BOOL)isGiftBadge; // used when restoring quoted messages from backups -// TODO: attachments should be here too, once they are body can be made nullable. + (instancetype)quotedMessageWithTargetMessageTimestamp:(nullable NSNumber *)timestamp authorAddress:(SignalServiceAddress *)authorAddress - body:(NSString *)body + body:(nullable NSString *)body bodyRanges:(nullable MessageBodyRanges *)bodyRanges bodySource:(TSQuotedMessageContentSource)bodySource + quotedAttachmentInfo:(nullable OWSAttachmentInfo *)attachmentInfo isGiftBadge:(BOOL)isGiftBadge; @end diff --git a/SignalServiceKit/Messages/Interactions/Quotes/TSQuotedMessage.m b/SignalServiceKit/Messages/Interactions/Quotes/TSQuotedMessage.m index 27954ade12..65183999e2 100644 --- a/SignalServiceKit/Messages/Interactions/Quotes/TSQuotedMessage.m +++ b/SignalServiceKit/Messages/Interactions/Quotes/TSQuotedMessage.m @@ -210,11 +210,13 @@ NS_ASSUME_NONNULL_BEGIN + (instancetype)quotedMessageWithTargetMessageTimestamp:(nullable NSNumber *)timestamp authorAddress:(SignalServiceAddress *)authorAddress - body:(NSString *)body + body:(nullable NSString *)body bodyRanges:(nullable MessageBodyRanges *)bodyRanges bodySource:(TSQuotedMessageContentSource)bodySource + quotedAttachmentInfo:(nullable OWSAttachmentInfo *)attachmentInfo isGiftBadge:(BOOL)isGiftBadge { + OWSAssertDebug(body != nil || attachmentInfo != nil); OWSAssertDebug(authorAddress.isValid); uint64_t rawTimestamp; @@ -230,7 +232,7 @@ NS_ASSUME_NONNULL_BEGIN body:body bodyRanges:bodyRanges bodySource:bodySource - receivedQuotedAttachmentInfo:nil + receivedQuotedAttachmentInfo:attachmentInfo isGiftBadge:isGiftBadge]; }