Signal-iOS/SignalServiceKit/MessageBackup/Archivers/ChatItem/RestoredSentMessageTranscript.swift
2024-07-11 13:15:41 -05:00

244 lines
9.1 KiB
Swift

//
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import Foundation
import LibSignalClient
/// Restoring an outgoing message from a backup isn't any different from learning about
/// an outgoing message sent on a linked device and synced to the local device.
///
/// So we represent restored messages as "transcripts" that we can plug into the same
/// transcript processing pipes as synced message transcripts.
internal class RestoredSentMessageTranscript: SentMessageTranscript {
let type: SentMessageTranscriptType
let timestamp: UInt64
// Not applicable
var requiredProtocolVersion: UInt32? { nil }
let recipientStates: [MessageBackup.InteropAddress: TSOutgoingMessageRecipientState]
internal static func from(
chatItem: BackupProto.ChatItem,
contents: MessageBackup.RestoredMessageContents,
outgoingDetails: BackupProto.ChatItem.OutgoingMessageDetails,
context: MessageBackup.ChatRestoringContext,
chatThread: MessageBackup.ChatThread
) -> MessageBackup.RestoreInteractionResult<RestoredSentMessageTranscript> {
let expirationToken: DisappearingMessageToken = .token(forProtoExpireTimerMillis: chatItem.expiresInMs)
let target: SentMessageTranscriptTarget
switch chatThread {
case .contact(let contactThread):
target = .contact(contactThread, expirationToken)
case .groupV2(let groupThread):
target = .group(groupThread)
}
let messageType: SentMessageTranscriptType
switch contents {
case .text(let text):
messageType = restoreMessageTranscript(
contents: text,
target: target,
chatItem: chatItem,
expirationToken: expirationToken
)
case .archivedPayment(let archivedPayment):
messageType = restorePaymentTranscript(
payment: archivedPayment,
target: target,
chatItem: chatItem,
expirationToken: expirationToken
)
}
var partialErrors = [MessageBackup.RestoreFrameError<MessageBackup.ChatItemId>]()
var recipientStates = [MessageBackup.InteropAddress: TSOutgoingMessageRecipientState]()
for sendStatus in outgoingDetails.sendStatus {
let recipientAddress: MessageBackup.InteropAddress
let recipientID = sendStatus.destinationRecipientId
switch context.recipientContext[recipientID] {
case .contact(let address):
recipientAddress = address.asInteropAddress()
case .none:
// Missing recipient! Fail this one recipient but keep going.
partialErrors.append(.restoreFrameError(
.invalidProtoData(.recipientIdNotFound(recipientID)),
chatItem.id
))
continue
case .localAddress, .group, .distributionList, .releaseNotesChannel:
// Recipients can only be contacts.
partialErrors.append(.restoreFrameError(
.invalidProtoData(.outgoingNonContactMessageRecipient),
chatItem.id
))
continue
}
guard
let recipientState = recipientState(
for: sendStatus,
partialErrors: &partialErrors,
chatItemId: chatItem.id
)
else {
continue
}
recipientStates[recipientAddress] = recipientState
}
if recipientStates.isEmpty && outgoingDetails.sendStatus.isEmpty.negated {
// We put up with some failures, but if we get no recipients at all
// fail the whole thing.
return .messageFailure(partialErrors)
}
let transcript = RestoredSentMessageTranscript(
type: messageType,
timestamp: chatItem.dateSent,
recipientStates: recipientStates
)
if partialErrors.isEmpty {
return .success(transcript)
} else {
return .partialRestore(transcript, partialErrors)
}
}
private static func restoreMessageTranscript(
contents: MessageBackup.RestoredMessageContents.Text,
target: SentMessageTranscriptTarget,
chatItem: BackupProto.ChatItem,
expirationToken: DisappearingMessageToken
) -> SentMessageTranscriptType {
// TODO: handle attachments in quotes
let quotedMessageBuilder = { [contents] (_: DBWriteTransaction) in
contents.quotedMessage.map {
return OwnedAttachmentBuilder<TSQuotedMessage>.withoutFinalizer($0)
}
}
let messageParams = SentMessageTranscriptType.Message(
target: target,
body: contents.body.text,
bodyRanges: contents.body.ranges,
// TODO: attachments
attachmentPointerProtos: [],
makeQuotedMessageBuilder: quotedMessageBuilder,
// TODO: contact message
makeContactBuilder: { _ in nil },
// TODO: linkPreview message
makeLinkPreviewBuilder: { _ in nil },
// TODO: gift badge message
giftBadge: nil,
// TODO: sticker message
makeMessageStickerBuilder: { _ in nil },
// TODO: isViewOnceMessage
isViewOnceMessage: false,
expirationStartedAt: chatItem.expireStartDate,
expirationDurationSeconds: expirationToken.durationSeconds,
// We never restore stories.
storyTimestamp: nil,
storyAuthorAci: nil
)
return .message(messageParams)
}
private static func restorePaymentTranscript(
payment: MessageBackup.RestoredMessageContents.Payment,
target: SentMessageTranscriptTarget,
chatItem: BackupProto.ChatItem,
expirationToken: DisappearingMessageToken
) -> SentMessageTranscriptType {
return .archivedPayment(
.init(
target: target,
amount: payment.amount,
fee: payment.fee,
note: payment.note,
expirationStartedAt: chatItem.expireStartDate,
expirationDurationSeconds: expirationToken.durationSeconds
)
)
}
private static func recipientState(
for sendStatus: BackupProto.SendStatus,
partialErrors: inout [MessageBackup.RestoreFrameError<MessageBackup.ChatItemId>],
chatItemId: MessageBackup.ChatItemId
) -> TSOutgoingMessageRecipientState? {
guard let recipientState = TSOutgoingMessageRecipientState() else {
partialErrors.append(.restoreFrameError(
.databaseInsertionFailed(OWSAssertionError("Unable to create recipient state!")),
chatItemId
))
return nil
}
recipientState.wasSentByUD = sendStatus.sealedSender.negated
switch sendStatus.deliveryStatus {
case nil, .UNKNOWN:
partialErrors.append(.restoreFrameError(.invalidProtoData(.unrecognizedMessageSendStatus), chatItemId))
return nil
case .PENDING:
recipientState.state = .pending
recipientState.errorCode = nil
return recipientState
case .SENT:
recipientState.state = .sent
recipientState.errorCode = nil
return recipientState
case .DELIVERED:
recipientState.state = .sent
recipientState.deliveryTimestamp = NSNumber(value: sendStatus.lastStatusUpdateTimestamp)
recipientState.errorCode = nil
return recipientState
case .READ:
recipientState.state = .sent
recipientState.readTimestamp = NSNumber(value: sendStatus.lastStatusUpdateTimestamp)
recipientState.errorCode = nil
return recipientState
case .VIEWED:
recipientState.state = .sent
recipientState.viewedTimestamp = NSNumber(value: sendStatus.lastStatusUpdateTimestamp)
recipientState.errorCode = nil
return recipientState
case .SKIPPED:
recipientState.state = .skipped
recipientState.errorCode = nil
return recipientState
case .FAILED:
recipientState.state = .failed
if sendStatus.identityKeyMismatch {
// We want to explicitly represent identity key errors.
// Other types we don't really care about.
recipientState.errorCode = NSNumber(value: UntrustedIdentityError.errorCode)
} else {
recipientState.errorCode = NSNumber(value: OWSErrorCode.genericFailure.rawValue)
}
return recipientState
}
}
private init(
type: SentMessageTranscriptType,
timestamp: UInt64,
recipientStates: [MessageBackup.InteropAddress: TSOutgoingMessageRecipientState]
) {
self.type = type
self.timestamp = timestamp
self.recipientStates = recipientStates
}
}