288 lines
10 KiB
Swift
288 lines
10 KiB
Swift
//
|
|
// Copyright 2023 Signal Messenger, LLC
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
//
|
|
|
|
import Foundation
|
|
|
|
extension MessageBackup {
|
|
|
|
public struct ChatItemId: ExpressibleByIntegerLiteral, Hashable {
|
|
|
|
public typealias IntegerLiteralType = UInt64
|
|
|
|
internal let value: UInt64
|
|
|
|
public init(integerLiteral value: UInt64) {
|
|
self.value = value
|
|
}
|
|
|
|
fileprivate init(_ value: UInt64) {
|
|
self.value = value
|
|
}
|
|
|
|
public init(interaction: TSInteraction) {
|
|
self.value = interaction.timestamp
|
|
}
|
|
}
|
|
|
|
internal enum ChatItemMessageType {
|
|
case standard(BackupProtoStandardMessage)
|
|
case contact(BackupProtoContactMessage)
|
|
case voice(BackupProtoVoiceMessage)
|
|
case sticker(BackupProtoStickerMessage)
|
|
case remotelyDeleted(BackupProtoRemoteDeletedMessage)
|
|
case chatUpdate(BackupProtoChatUpdateMessage)
|
|
|
|
init?(_ chatItem: BackupProtoChatItem) {
|
|
if let standardMessage = chatItem.standardMessage {
|
|
self = .standard(standardMessage)
|
|
} else if let contactMessage = chatItem.contactMessage {
|
|
self = .contact(contactMessage)
|
|
} else if let voiceMessage = chatItem.voiceMessage {
|
|
self = .voice(voiceMessage)
|
|
} else if let stickerMessage = chatItem.stickerMessage {
|
|
self = .sticker(stickerMessage)
|
|
} else if let remoteDeletedMessage = chatItem.remoteDeletedMessage {
|
|
self = .remotelyDeleted(remoteDeletedMessage)
|
|
} else if let updateMessage = chatItem.updateMessage {
|
|
self = .chatUpdate(updateMessage)
|
|
} else {
|
|
owsFailDebug("Unknown chat item type!")
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
|
|
internal struct InteractionArchiveDetails {
|
|
enum DirectionalDetails {
|
|
case incoming(BackupProtoChatItemIncomingMessageDetails)
|
|
case outgoing(BackupProtoChatItemOutgoingMessageDetails)
|
|
case directionless(BackupProtoChatItemDirectionlessMessageDetails)
|
|
}
|
|
|
|
let author: RecipientId
|
|
let directionalDetails: DirectionalDetails
|
|
let expireStartDate: UInt64?
|
|
let expiresInMs: UInt64?
|
|
// TODO: edit revisions
|
|
let revisions: [BackupProtoChatItem] = []
|
|
// TODO: sms
|
|
let isSms: Bool = false
|
|
let isSealedSender: Bool
|
|
let type: ChatItemMessageType
|
|
}
|
|
|
|
enum SkippableGroupUpdate {
|
|
/// This is a group update from back when we kept raw strings on disk, instead
|
|
/// of metadata required to construct the string. We knowingly drop these.
|
|
case legacyRawString
|
|
/// In backups, we collapse the `inviteFriendsToNewlyCreatedGroup` into
|
|
/// the `createdByLocalUser`; the former is just omitted from backups.
|
|
case inviteFriendsToNewlyCreatedGroup
|
|
}
|
|
|
|
internal enum ArchiveInteractionResult<Component> {
|
|
typealias Error = MessageBackupChatItemArchiver.ArchiveMultiFrameResult.Error
|
|
|
|
case success(Component)
|
|
|
|
// MARK: Skips
|
|
|
|
/// This is a past revision that was since edited; can be safely skipped, as its
|
|
/// contents will be represented in the latest revision.
|
|
case isPastRevision
|
|
|
|
/// Some group updates are deliberately skipped; see sub-enum for reasons.
|
|
case skippableGroupUpdate(SkippableGroupUpdate)
|
|
|
|
// TODO: remove this once we flesh out implementation for all interactions.
|
|
case notYetImplemented
|
|
|
|
// MARK: Errors
|
|
|
|
/// Some portion of the interaction failed to archive, but we can still archive the rest of it.
|
|
/// e.g. some recipient details are missing, so we archive without that recipient.
|
|
case partialFailure(Component, [Error])
|
|
/// The entire message failed and should be skipped.
|
|
/// Other messages are unaffected.
|
|
case messageFailure([Error])
|
|
/// Catastrophic failure, which should stop _all_ message archiving.
|
|
case completeFailure(Swift.Error)
|
|
}
|
|
|
|
internal enum RestoreInteractionResult<Component> {
|
|
case success(Component)
|
|
/// Some portion of the interaction failed to restore, but we can still restore the rest of it.
|
|
/// e.g. a reaction failed to parse, so we just drop that reaction.
|
|
case partialRestore(Component, [RestoringFrameError])
|
|
/// The entire message failed and should be skipped.
|
|
/// Other messages are unaffected.
|
|
case messageFailure([RestoringFrameError])
|
|
}
|
|
}
|
|
|
|
internal protocol MessageBackupInteractionArchiver: MessageBackupProtoArchiver {
|
|
|
|
typealias Details = MessageBackup.InteractionArchiveDetails
|
|
|
|
static func canArchiveInteraction(_ interaction: TSInteraction) -> Bool
|
|
|
|
func archiveInteraction(
|
|
_ interaction: TSInteraction,
|
|
context: MessageBackup.ChatArchivingContext,
|
|
tx: DBReadTransaction
|
|
) -> MessageBackup.ArchiveInteractionResult<Details>
|
|
|
|
static func canRestoreChatItem(_ chatItem: BackupProtoChatItem) -> Bool
|
|
|
|
func restoreChatItem(
|
|
_ chatItem: BackupProtoChatItem,
|
|
thread: MessageBackup.ChatThread,
|
|
context: MessageBackup.ChatRestoringContext,
|
|
tx: DBWriteTransaction
|
|
) -> MessageBackup.RestoreInteractionResult<Void>
|
|
}
|
|
|
|
extension MessageBackup.ArchiveInteractionResult {
|
|
|
|
enum BubbleUp<T, E> {
|
|
case `continue`(T)
|
|
case bubbleUpError(MessageBackup.ArchiveInteractionResult<E>)
|
|
}
|
|
|
|
/// Make it easier to "bubble up" an error case of ``ArchiveInteractionResult`` thrown deeper in the call stack.
|
|
/// Basically, collapses all the cases that should just be bubbled up to the caller (error cases) into an easily returnable case,
|
|
/// ditto for the success or partial success cases, and handles updating partialErrors along the way.
|
|
///
|
|
/// Concretely, turns this:
|
|
///
|
|
/// switch someResult {
|
|
/// case .success(let value):
|
|
/// myVar = value
|
|
/// case .partialFailure(let value, let errors):
|
|
/// myVar = value
|
|
/// partialErrors.append(contentsOf: errors)
|
|
/// case someFailureCase(let someErrorOrErrors)
|
|
/// let coalescedErrorOrErrors = partialErrors.coalesceSomehow(with: someErrorOrErrors)
|
|
/// // Just bubble up the error after coalescing
|
|
/// return .someFailureCase(coalescedErrorOrErrors)
|
|
/// // ...
|
|
/// // The same for every other error case that should be bubbled up
|
|
/// // ...
|
|
/// }
|
|
///
|
|
/// Into this:
|
|
///
|
|
/// switch someResult.bubbleUp(&partialErrors) {
|
|
/// case .success(let value):
|
|
/// myVar = value
|
|
/// case .bubbleUpError(let error):
|
|
/// return error
|
|
/// }
|
|
func bubbleUp<ErrorResultType>(
|
|
_ resultType: ErrorResultType.Type = ErrorResultType.self,
|
|
partialErrors: inout [Error]
|
|
) -> BubbleUp<Component, ErrorResultType> {
|
|
switch self {
|
|
case .success(let value):
|
|
return .continue(value)
|
|
|
|
case .partialFailure(let value, let errors):
|
|
// Continue through partial failures.
|
|
partialErrors.append(contentsOf: errors)
|
|
return .continue(value)
|
|
|
|
// These types are just bubbled up as-is
|
|
case .isPastRevision:
|
|
return .bubbleUpError(.isPastRevision)
|
|
case .skippableGroupUpdate(let groupUpdate):
|
|
return .bubbleUpError(.skippableGroupUpdate(groupUpdate))
|
|
case .notYetImplemented:
|
|
return .bubbleUpError(.notYetImplemented)
|
|
case .completeFailure(let error):
|
|
return .bubbleUpError(.completeFailure(error))
|
|
|
|
case .messageFailure(let errors):
|
|
// Add message failure to partial errors and bubble it up.
|
|
partialErrors.append(contentsOf: errors)
|
|
return .bubbleUpError(.messageFailure(partialErrors))
|
|
}
|
|
}
|
|
}
|
|
|
|
extension MessageBackup.RestoreInteractionResult {
|
|
|
|
/// Returns nil for ``RestoreInteractionResult.messageFailure``, otherwise
|
|
/// returns the restored component. Regardless, accumulates any errors so that the caller
|
|
/// can return the passed in ``partialErrors`` array in the final result.
|
|
///
|
|
/// Concretely, turns this:
|
|
///
|
|
/// switch someResult {
|
|
/// case .success(let value):
|
|
/// myVar = value
|
|
/// case .partialRestore(let value, let errors):
|
|
/// myVar = value
|
|
/// partialErrors.append(contentsOf: errors)
|
|
/// case messageFailure(let errors)
|
|
/// partialErrors.append(contentsOf: errors)
|
|
/// return .messageFailure(partialErrors)
|
|
/// }
|
|
///
|
|
/// Into this:
|
|
///
|
|
/// guard let myVar = someResult.unwrap(&partialErrors) else {
|
|
/// return .messageFailure(partialErrors)
|
|
/// }
|
|
func unwrap(partialErrors: inout [MessageBackup.RestoringFrameError]) -> Component? {
|
|
switch self {
|
|
case .success(let component):
|
|
return component
|
|
case .partialRestore(let component, let errors):
|
|
partialErrors.append(contentsOf: errors)
|
|
return component
|
|
case .messageFailure(let errors):
|
|
partialErrors.append(contentsOf: errors)
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
|
|
extension MessageBackup.RestoreInteractionResult where Component == Void {
|
|
|
|
/// Returns false for ``RestoreInteractionResult.messageFailure``, otherwise
|
|
/// returns true. Regardless, accumulates any errors so that the caller
|
|
/// can return the passed in ``partialErrors`` array in the final result.
|
|
func unwrap(partialErrors: inout [MessageBackup.RestoringFrameError]) -> Bool {
|
|
switch self {
|
|
case .success:
|
|
return true
|
|
case .partialRestore(_, let errors):
|
|
partialErrors.append(contentsOf: errors)
|
|
return true
|
|
case .messageFailure(let errors):
|
|
partialErrors.append(contentsOf: errors)
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
|
|
extension BackupProtoChatItem {
|
|
|
|
var id: MessageBackup.ChatItemId {
|
|
return .init(self.dateSent)
|
|
}
|
|
|
|
var messageType: MessageBackup.ChatItemMessageType? {
|
|
return .init(self)
|
|
}
|
|
}
|
|
|
|
extension TSInteraction {
|
|
|
|
var chatItemId: MessageBackup.ChatItemId {
|
|
return .init(interaction: self)
|
|
}
|
|
}
|