Signal-iOS/SignalServiceKit/Messages/Attachments/V2/ViewOnce/TSResource/TSResourceViewOnceManagerImpl.swift
2024-05-22 17:20:38 -05:00

167 lines
7.0 KiB
Swift

//
// Copyright 2024 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import Foundation
import SignalCoreKit
public class TSResourceViewOnceManagerImpl: TSResourceViewOnceManager {
private let attachmentViewOnceManager: AttachmentViewOnceManager
private let db: DB
public init(
attachmentViewOnceManager: AttachmentViewOnceManager,
db: DB
) {
self.attachmentViewOnceManager = attachmentViewOnceManager
self.db = db
}
public func prepareViewOnceContentForDisplay(_ message: TSMessage) -> TSViewOnceContent? {
if message.attachmentIds.isEmpty {
return attachmentViewOnceManager.prepareViewOnceContentForDisplay(message)?.asTSContent
}
var content: TSViewOnceContent?
// The only way to ensure that the content is never presented
// more than once is to do a bunch of work (include file system
// activity) inside a write transaction, which normally
// wouldn't be desirable.
db.write { transaction in
let transaction = SDSDB.shimOnlyBridge(transaction)
let interactionId = message.uniqueId
guard let message = TSInteraction.anyFetch(uniqueId: interactionId, transaction: transaction) as? TSMessage else {
return
}
guard message.isViewOnceMessage else {
owsFailDebug("Unexpected message.")
return
}
let messageId = message.uniqueId
// Auto-complete the message before going any further.
ViewOnceMessages.completeIfNecessary(message: message, transaction: transaction)
guard !message.isViewOnceComplete else {
return
}
// We should _always_ mark the message as complete,
// even if the message is malformed, or if we fail
// to do the "file system dance" below, etc.
// and we fail to present the message content.
defer {
// This will eliminate the renderable content of the message.
ViewOnceMessages.markAsComplete(message: message, sendSyncMessages: true, transaction: transaction)
}
guard
let firstAttachmentId = message.attachmentIds.first,
let attachmentStream = TSAttachmentStream.anyFetchAttachmentStream(
uniqueId: firstAttachmentId,
transaction: transaction
)
else {
return
}
guard attachmentStream.computeIsValidVisualMedia() else {
return
}
let mimeType = attachmentStream.mimeType
if mimeType.isEmpty {
owsFailDebug("Missing mime type.")
return
}
let viewOnceType: TSViewOnceContent.ContentType
switch attachmentStream.computeContentType() {
case .animatedImage:
viewOnceType = .animatedImage
case .image:
viewOnceType = .stillImage
case .video where attachmentStream.attachmentType.asRenderingFlag == .shouldLoop:
viewOnceType = .loopingVideo
case .video:
viewOnceType = .video
case .audio, .file, .invalid:
owsFailDebug("Unexpected content type.")
return
}
// To ensure that we never show the content more than once,
// we mark the "view-once message" as complete _before_
// presenting its contents. A side effect of this is that
// its renderable content is deleted. We need the renderable
// content to present it. Therefore, we do a little dance:
//
// * Move the attachment file to a temporary file.
// * Create an empty placeholder file in the old attachment
// file's location so that TSAttachmentStream's invariant
// of always corresponding to an underlying file on disk
// remains true.
// * Delete the temporary file when this view is dismissed.
// * If the app terminates at any step during this process,
// either: a) the file wasn't moved, the message wasn't
// marked as complete and the content wasn't displayed
// so the user can try again after relaunch.
// b) the file was moved and will be cleaned up on next
// launch like any other temp file if it hasn't been
// deleted already.
guard let originalFilePath = attachmentStream.originalFilePath else {
owsFailDebug("Attachment missing file path.")
return
}
guard OWSFileSystem.fileOrFolderExists(atPath: originalFilePath) else {
owsFailDebug("Missing attachment file.")
return
}
guard let fileExtension = MimeTypeUtil.fileExtensionForMimeType(mimeType) else {
owsFailDebug("Couldn't determine file extension.")
return
}
let tempFilePath = OWSFileSystem.temporaryFilePath(fileExtension: fileExtension)
// Move the attachment to the temp file.
do {
try OWSFileSystem.moveFilePath(originalFilePath, toFilePath: tempFilePath)
} catch {
owsFailDebug("Couldn't move file: \(error.shortDescription)")
return
}
guard OWSFileSystem.fileOrFolderExists(atPath: tempFilePath) else {
owsFailDebug("Missing temp file.")
return
}
// This should be redundant since temp files are
// created inside the per-launch temp folder
// and should inherit protection from it.
guard OWSFileSystem.protectFileOrFolder(atPath: tempFilePath) else {
owsFailDebug("Couldn't protect temp file.")
OWSFileSystem.deleteFile(tempFilePath)
return
}
// Create new empty "placeholder file at the attachment's old
// location, since the attachment model should always correspond
// to an underlying file on disk.
guard OWSFileSystem.ensureFileExists(originalFilePath) else {
owsFailDebug("Couldn't create placeholder file.")
OWSFileSystem.deleteFile(tempFilePath)
return
}
guard OWSFileSystem.fileOrFolderExists(atPath: originalFilePath) else {
owsFailDebug("Missing placeholder file.")
OWSFileSystem.deleteFile(tempFilePath)
return
}
content = TSViewOnceContent(
messageId: messageId,
type: viewOnceType,
unencryptedFileUrl: URL(fileURLWithPath: tempFilePath),
mimeType: mimeType
)
}
return content
}
}