Signal-iOS/SignalServiceKit/MessageBackup/Archivers/ChatItem/MessageBackupTSOutgoingMessageArchiver.swift
2024-05-06 11:43:13 -07:00

264 lines
10 KiB
Swift

//
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import Foundation
import LibSignalClient
internal class MessageBackupTSOutgoingMessageArchiver: MessageBackupInteractionArchiver {
static let archiverType: MessageBackup.ChatItemArchiverType = .outgoingMessage
private let contentsArchiver: MessageBackupTSMessageContentsArchiver
private let interactionStore: InteractionStore
private let sentMessageTranscriptReceiver: SentMessageTranscriptReceiver
internal init(
contentsArchiver: MessageBackupTSMessageContentsArchiver,
interactionStore: InteractionStore,
sentMessageTranscriptReceiver: SentMessageTranscriptReceiver
) {
self.contentsArchiver = contentsArchiver
self.interactionStore = interactionStore
self.sentMessageTranscriptReceiver = sentMessageTranscriptReceiver
}
// MARK: - Archiving
func archiveInteraction(
_ interaction: TSInteraction,
context: MessageBackup.ChatArchivingContext,
tx: DBReadTransaction
) -> MessageBackup.ArchiveInteractionResult<Details> {
guard let message = interaction as? TSOutgoingMessage else {
// Should be impossible.
return .completeFailure(.fatalArchiveError(.developerError(
OWSAssertionError("Invalid interaction type")
)))
}
var partialErrors = [MessageBackupChatItemArchiver.ArchiveMultiFrameResult.ArchiveFrameError]()
let wasAnySendSealedSender: Bool
let directionalDetails: Details.DirectionalDetails
switch buildOutgoingMessageDetails(
message,
recipientContext: context.recipientContext
).bubbleUp(Details.self, partialErrors: &partialErrors) {
case .continue(let details):
directionalDetails = details.details
wasAnySendSealedSender = details.wasAnySendSealedSender
case .bubbleUpError(let errorResult):
return errorResult
}
let contentsResult = contentsArchiver.archiveMessageContents(
message,
context: context.recipientContext,
tx: tx
)
let chatItemType: MessageBackup.InteractionArchiveDetails.ChatItemType
switch contentsResult.bubbleUp(Details.self, partialErrors: &partialErrors) {
case .continue(let t):
chatItemType = t
case .bubbleUpError(let errorResult):
return errorResult
}
let details = Details(
author: context.recipientContext.localRecipientId,
directionalDetails: directionalDetails,
expireStartDate: message.expireStartedAt,
expiresInMs: UInt64(message.expiresInSeconds) * 1000,
isSealedSender: wasAnySendSealedSender,
chatItemType: chatItemType
)
if partialErrors.isEmpty {
return .success(details)
} else {
return .partialFailure(details, partialErrors)
}
}
struct OutgoingMessageDetails {
let details: Details.DirectionalDetails
let wasAnySendSealedSender: Bool
}
private func buildOutgoingMessageDetails(
_ message: TSOutgoingMessage,
recipientContext: MessageBackup.RecipientArchivingContext
) -> MessageBackup.ArchiveInteractionResult<OutgoingMessageDetails> {
var perRecipientErrors = [MessageBackup.ArchiveInteractionResult<Details.DirectionalDetails>.ArchiveFrameError]()
var wasAnySendSealedSender = false
var outgoingMessage = BackupProto.ChatItem.OutgoingMessageDetails()
for (address, sendState) in message.recipientAddressStates ?? [:] {
guard let recipientAddress = address.asSingleServiceIdBackupAddress()?.asArchivingAddress() else {
perRecipientErrors.append(.archiveFrameError(
.invalidOutgoingMessageRecipient,
message.uniqueInteractionId
))
continue
}
guard let recipientId = recipientContext[recipientAddress] else {
perRecipientErrors.append(.archiveFrameError(
.referencedRecipientIdMissing(recipientAddress),
message.uniqueInteractionId
))
continue
}
var isNetworkFailure = false
var isIdentityKeyMismatchFailure = false
let protoDeliveryStatus: BackupProto.SendStatus.Status
let statusTimestamp: UInt64
switch sendState.state {
case OWSOutgoingMessageRecipientState.sent:
if let readTimestamp = sendState.readTimestamp {
protoDeliveryStatus = .READ
statusTimestamp = readTimestamp.uint64Value
} else if let viewedTimestamp = sendState.viewedTimestamp {
protoDeliveryStatus = .VIEWED
statusTimestamp = viewedTimestamp.uint64Value
} else if let deliveryTimestamp = sendState.deliveryTimestamp {
protoDeliveryStatus = .DELIVERED
statusTimestamp = deliveryTimestamp.uint64Value
} else {
protoDeliveryStatus = .SENT
statusTimestamp = message.timestamp
}
case OWSOutgoingMessageRecipientState.failed:
// TODO: identify specific errors. for now call everything network.
isNetworkFailure = true
isIdentityKeyMismatchFailure = false
protoDeliveryStatus = .FAILED
statusTimestamp = message.timestamp
case OWSOutgoingMessageRecipientState.sending, OWSOutgoingMessageRecipientState.pending:
protoDeliveryStatus = .PENDING
statusTimestamp = message.timestamp
case OWSOutgoingMessageRecipientState.skipped:
protoDeliveryStatus = .SKIPPED
statusTimestamp = message.timestamp
}
var sendStatus = BackupProto.SendStatus(
recipientId: recipientId.value,
networkFailure: isNetworkFailure,
identityKeyMismatch: isIdentityKeyMismatchFailure,
sealedSender: sendState.wasSentByUD.negated,
lastStatusUpdateTimestamp: statusTimestamp
)
sendStatus.deliveryStatus = protoDeliveryStatus
outgoingMessage.sendStatus.append(sendStatus)
if sendState.wasSentByUD.negated {
wasAnySendSealedSender = true
}
}
if perRecipientErrors.isEmpty {
return .success(.init(
details: .outgoing(outgoingMessage),
wasAnySendSealedSender: wasAnySendSealedSender
))
} else {
return .partialFailure(
.init(
details: .outgoing(outgoingMessage),
wasAnySendSealedSender: wasAnySendSealedSender
),
perRecipientErrors
)
}
}
// MARK: - Restoring
func restoreChatItem(
_ chatItem: BackupProto.ChatItem,
thread: MessageBackup.ChatThread,
context: MessageBackup.ChatRestoringContext,
tx: DBWriteTransaction
) -> MessageBackup.RestoreInteractionResult<Void> {
let outgoingDetails: BackupProto.ChatItem.OutgoingMessageDetails
switch chatItem.directionalDetails {
case .outgoing(let backupProtoChatItemOutgoingMessageDetails):
outgoingDetails = backupProtoChatItemOutgoingMessageDetails
case nil, .incoming, .directionless:
// Should be impossible.
return .messageFailure([.restoreFrameError(
.developerError(OWSAssertionError("OutgoingMessageArchiver given non-outgoing message!")),
chatItem.id
)])
}
guard let chatItemType = chatItem.item else {
// Unrecognized item type!
return .messageFailure([.restoreFrameError(.invalidProtoData(.unrecognizedChatItemType), chatItem.id)])
}
var partialErrors = [MessageBackup.RestoreFrameError<MessageBackup.ChatItemId>]()
let contentsResult = contentsArchiver.restoreContents(
chatItemType,
chatItemId: chatItem.id,
thread: thread,
context: context,
tx: tx
)
guard let contents = contentsResult.unwrap(partialErrors: &partialErrors) else {
return .messageFailure(partialErrors)
}
let transcriptResult = RestoredSentMessageTranscript.from(
chatItem: chatItem,
contents: contents,
outgoingDetails: outgoingDetails,
context: context,
thread: thread
)
guard let transcript = transcriptResult.unwrap(partialErrors: &partialErrors) else {
return .messageFailure(partialErrors)
}
let messageResult = sentMessageTranscriptReceiver.process(
transcript,
localIdentifiers: context.recipientContext.localIdentifiers,
tx: tx
)
let message: TSOutgoingMessage
switch messageResult {
case .success(let outgoingMessage):
guard let outgoingMessage else {
return .messageFailure(partialErrors)
}
message = outgoingMessage
case .failure(let error):
partialErrors.append(.restoreFrameError(.databaseInsertionFailed(error), chatItem.id))
return .messageFailure(partialErrors)
}
let downstreamObjectsResult = contentsArchiver.restoreDownstreamObjects(
message: message,
chatItemId: chatItem.id,
restoredContents: contents,
context: context,
tx: tx
)
guard downstreamObjectsResult.unwrap(partialErrors: &partialErrors) else {
return .messageFailure(partialErrors)
}
if partialErrors.isEmpty {
return .success(())
} else {
return .partialRestore((), partialErrors)
}
}
}