TM-SGNL-iOS/SignalServiceKit/Messages/Attachments/V2/ViewOnce/AttachmentViewOnceManagerImpl.swift
TeleMessage developers dde0620daf initial commit
2025-05-03 12:28:28 -07:00

137 lines
5.2 KiB
Swift

//
// Copyright 2024 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import Foundation
public class AttachmentViewOnceManagerImpl: AttachmentViewOnceManager {
private let attachmentStore: AttachmentStore
private let db: any DB
private let interactionStore: InteractionStore
public init(
attachmentStore: AttachmentStore,
db: any DB,
interactionStore: InteractionStore
) {
self.attachmentStore = attachmentStore
self.db = db
self.interactionStore = interactionStore
}
public func prepareViewOnceContentForDisplay(_ message: TSMessage) -> ViewOnceContent? {
guard let messageRowId = message.sqliteRowId else {
// Don't ever display uninserted view once messages; we can't lock without the db.
return nil
}
// Re-fetch the message.
let message = db.read { tx in
self.interactionStore.fetchInteraction(rowId: messageRowId, tx: tx) as? TSMessage
}
guard let message else {
return nil
}
guard message.isViewOnceMessage else {
owsFailDebug("Unexpected view once message.")
return nil
}
// 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 {
db.write { tx in
// This will eliminate the renderable content of the message.
ViewOnceMessages.markAsComplete(
message: message,
sendSyncMessages: true,
transaction: SDSDB.shimOnlyBridge(tx)
)
}
}
let attachment = db.read { tx in
return attachmentStore.fetchFirstReferencedAttachment(
for: .messageBodyAttachment(messageRowId: messageRowId),
tx: tx
)
}
guard let attachmentStream = attachment?.asReferencedStream else {
owsFailDebug("Viewing unavailable view once attachment")
return nil
}
let viewOnceType: ViewOnceContent.ContentType
switch attachmentStream.attachmentStream.contentType {
case .file, .invalid, .audio:
owsFailDebug("Unexpected content type.")
return nil
case .animatedImage:
viewOnceType = .animatedImage
case .image:
viewOnceType = .stillImage
case .video where attachmentStream.reference.renderingFlag == .shouldLoop:
viewOnceType = .loopingVideo
case .video:
viewOnceType = .video
}
// 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.
// * Delete the temporary file when done displaying (handled by ViewOnceContent).
// * 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.
let originalFileUrl = attachmentStream.attachmentStream.fileURL
guard OWSFileSystem.fileOrFolderExists(url: originalFileUrl) else {
owsFailDebug("Missing attachment file.")
return nil
}
let tempFileUrl = OWSFileSystem.temporaryFileUrl()
guard !OWSFileSystem.fileOrFolderExists(url: tempFileUrl) else {
owsFailDebug("Temp file unexpectedly already exists.")
return nil
}
// Copy the attachment to the temp file.
do {
try OWSFileSystem.copyFile(from: originalFileUrl, to: tempFileUrl)
} catch {
owsFailDebug("Couldn't copy file.")
return nil
}
guard OWSFileSystem.fileOrFolderExists(url: tempFileUrl) else {
owsFailDebug("Missing temp file.")
return nil
}
// 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: tempFileUrl.path) else {
owsFailDebug("Couldn't protect temp file.")
try? OWSFileSystem.deleteFile(url: tempFileUrl)
return nil
}
return ViewOnceContent(
messageId: message.uniqueId,
type: viewOnceType,
fileUrl: tempFileUrl,
encryptionKey: attachmentStream.attachmentStream.attachment.encryptionKey,
plaintextLength: attachmentStream.attachmentStream.info.unencryptedByteCount,
mimeType: attachmentStream.attachmentStream.mimeType
)
}
}