244 lines
9.1 KiB
Swift
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
|
|
}
|
|
}
|