Signal-iOS/SignalServiceKit/MessageBackup/Archivers/ChatItem/RestoredSentMessageTranscript.swift
2024-04-01 15:04:24 -07:00

201 lines
7.5 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 {
private let messageParams: SentMessageTranscriptType.Message
var type: SentMessageTranscriptType {
return .message(messageParams)
}
let timestamp: UInt64
// Not applicable
var requiredProtocolVersion: UInt32? { nil }
let recipientStates: [MessageBackup.InteropAddress: TSOutgoingMessageRecipientState]
internal static func from(
chatItem: BackupProtoChatItem,
contents: MessageBackup.RestoredMessageContents,
outgoingDetails: BackupProtoChatItem.BackupProtoOutgoingMessageDetails,
context: MessageBackup.ChatRestoringContext,
thread: MessageBackup.ChatThread
) -> MessageBackup.RestoreInteractionResult<RestoredSentMessageTranscript> {
let expirationDuration: UInt32? = chatItem.expiresInMs.map { UInt32($0 / 1000) }
let target: SentMessageTranscriptTarget
switch thread {
case .contact(let contactThread):
target = .contact(contactThread, .token(forProtoExpireTimer: expirationDuration))
case .groupV2(let groupThread):
target = .group(groupThread)
}
// TODO: handle attachments in quotes
let quotedMessageBuilder = 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: [],
quotedMessageBuilder: quotedMessageBuilder,
// TODO: contact message
contact: nil,
// TODO: linkPreview message
linkPreviewBuilder: nil,
// TODO: gift badge message
giftBadge: nil,
// TODO: sticker message
messageStickerBuilder: nil,
// TODO: isViewOnceMessage
isViewOnceMessage: false,
expirationStartedAt: chatItem.expireStartDate,
expirationDuration: expirationDuration,
// We never restore stories.
storyTimestamp: nil,
storyAuthorAci: nil
)
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(.invalidProtoData(
chatItem.id,
.recipientIdNotFound(recipientID)
))
continue
case .localAddress, .group:
// Recipients can only be contacts.
partialErrors.append(.invalidProtoData(
chatItem.id,
.outgoingNonContactMessageRecipient
))
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(
messageParams: messageParams,
timestamp: chatItem.dateSent,
recipientStates: recipientStates
)
if partialErrors.isEmpty {
return .success(transcript)
} else {
return .partialRestore(transcript, partialErrors)
}
}
private static func recipientState(
for sendStatus: BackupProtoSendStatus,
partialErrors: inout [MessageBackup.RestoreFrameError<MessageBackup.ChatItemId>],
chatItemId: MessageBackup.ChatItemId
) -> TSOutgoingMessageRecipientState? {
guard let recipientState = TSOutgoingMessageRecipientState() else {
partialErrors.append(.databaseInsertionFailed(
chatItemId,
OWSAssertionError("Unable to create recipient state!")
))
return nil
}
recipientState.wasSentByUD = sendStatus.sealedSender.negated
switch sendStatus.deliveryStatus {
case nil, .UNKNOWN:
partialErrors.append(.invalidProtoData(chatItemId, .unrecognizedMessageSendStatus))
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(
messageParams: SentMessageTranscriptType.Message,
timestamp: UInt64,
recipientStates: [MessageBackup.InteropAddress: TSOutgoingMessageRecipientState]
) {
self.messageParams = messageParams
self.timestamp = timestamp
self.recipientStates = recipientStates
}
}