Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
signalapp
GitHub Repository: signalapp/Signal-iOS
Path: blob/main/SignalServiceKit/Messages/DeviceSyncing/DeleteForMe/DeleteForMeOutgoingSyncMessage.swift
1 views
//
// Copyright 2024 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//

import LibSignalClient

/// Represents a sync message, being sent from this device, related to "delete
/// for me" actions.
///
/// - SeeAlso ``DeleteForMeOutgoingSyncMessageManager``
@objc(DeleteForMeOutgoingSyncMessage)
final class DeleteForMeOutgoingSyncMessage: OutgoingSyncMessage {
    override class var supportsSecureCoding: Bool { true }

    required init?(coder: NSCoder) {
        guard let contents = coder.decodeObject(of: NSData.self, forKey: "contents") as Data? else {
            return nil
        }
        self.contents = contents
        super.init(coder: coder)
    }

    override func encode(with coder: NSCoder) {
        super.encode(with: coder)
        coder.encode(contents, forKey: "contents")
    }

    override var hash: Int {
        var hasher = Hasher()
        hasher.combine(super.hash)
        hasher.combine(contents)
        return hasher.finalize()
    }

    override func isEqual(_ object: Any?) -> Bool {
        guard let object = object as? Self else { return false }
        guard super.isEqual(object) else { return false }
        guard self.contents == object.contents else { return false }
        return true
    }

    typealias Outgoing = DeleteForMeSyncMessage.Outgoing

    struct Contents: Codable {
        private enum CodingKeys: String, CodingKey {
            case messageDeletes
            case attachmentDeletes
            case conversationDeletes
            case localOnlyConversationDelete
        }

        let messageDeletes: [Outgoing.MessageDeletes]
        /// Attachment deletes were added after this type may already have been
        /// persisted, so this needs to be an optional property that decodes as
        /// `nil` from existing data.
        let attachmentDeletes: [Outgoing.AttachmentDelete]?
        let conversationDeletes: [Outgoing.ConversationDelete]
        let localOnlyConversationDelete: [Outgoing.LocalOnlyConversationDelete]

#if TESTABLE_BUILD
        init(
            messageDeletes: [Outgoing.MessageDeletes],
            nilAttachmentDeletes: Void,
            conversationDeletes: [Outgoing.ConversationDelete],
            localOnlyConversationDelete: [Outgoing.LocalOnlyConversationDelete],
        ) {
            self.messageDeletes = messageDeletes
            self.attachmentDeletes = nil
            self.conversationDeletes = conversationDeletes
            self.localOnlyConversationDelete = localOnlyConversationDelete
        }
#endif

        init(
            messageDeletes: [Outgoing.MessageDeletes],
            attachmentDeletes: [Outgoing.AttachmentDelete],
            conversationDeletes: [Outgoing.ConversationDelete],
            localOnlyConversationDelete: [Outgoing.LocalOnlyConversationDelete],
        ) {
            self.attachmentDeletes = attachmentDeletes
            self.messageDeletes = messageDeletes
            self.conversationDeletes = conversationDeletes
            self.localOnlyConversationDelete = localOnlyConversationDelete
        }

        fileprivate var asProto: SSKProtoSyncMessageDeleteForMe {
            let protoBuilder = SSKProtoSyncMessageDeleteForMe.builder()
            protoBuilder.setMessageDeletes(messageDeletes.map { $0.asProto })
            if let attachmentDeletes {
                protoBuilder.setAttachmentDeletes(attachmentDeletes.map { $0.asProto })
            }
            protoBuilder.setConversationDeletes(conversationDeletes.map { $0.asProto })
            protoBuilder.setLocalOnlyConversationDeletes(localOnlyConversationDelete.map { $0.asProto })
            return protoBuilder.buildInfallibly()
        }
    }

    /// A JSON-serialized ``Contents`` struct.
    private(set) var contents: Data

    init?(
        contents: Contents,
        localThread: TSContactThread,
        tx: DBReadTransaction,
    ) {
        do {
            self.contents = try JSONEncoder().encode(contents)
        } catch {
            owsFailDebug("Failed to encode sync message contents!")
            return nil
        }

        super.init(localThread: localThread, tx: tx)
    }

    override var isUrgent: Bool { false }

    override func syncMessageBuilder(tx: DBReadTransaction) -> SSKProtoSyncMessageBuilder? {
        let contents: Contents
        do {
            contents = try JSONDecoder().decode(Contents.self, from: self.contents)
        } catch let error {
            owsFailDebug("Failed to decode serialized sync message contents! \(error)")
            return nil
        }

        let syncMessageBuilder = SSKProtoSyncMessage.builder()
        syncMessageBuilder.setDeleteForMe(contents.asProto)
        return syncMessageBuilder
    }
}

// MARK: -

extension DeleteForMeSyncMessage.Outgoing {

    enum CodableConversationIdentifier: Codable, Equatable {
        case threadServiceId(serviceId: ServiceIdUppercaseString<ServiceId>)
        case threadE164(e164: String)
        case threadGroupId(groupId: Data)

        init(_ conversationIdentifier: ConversationIdentifier) {
            switch conversationIdentifier {
            case .serviceId(let serviceId):
                self = .threadServiceId(serviceId: ServiceIdUppercaseString(wrappedValue: serviceId))
            case .e164(let e164):
                self = .threadE164(e164: e164.stringValue)
            case .groupIdentifier(let groupIdentifier):
                self = .threadGroupId(groupId: groupIdentifier.serialize())
            }
        }

        var asProto: SSKProtoConversationIdentifier {
            let protoBuilder = SSKProtoConversationIdentifier.builder()
            switch self {
            case .threadServiceId(let serviceId):
                protoBuilder.setThreadServiceIDBinary(serviceId.wrappedValue.serviceIdBinary)
            case .threadE164(let e164): protoBuilder.setThreadE164(e164)
            case .threadGroupId(let groupId): protoBuilder.setThreadGroupID(groupId)
            }
            return protoBuilder.buildInfallibly()
        }
    }

    struct CodableAddressableMessage: Codable, Equatable {
        enum Author: Codable, Equatable {
            case aci(aci: ServiceIdUppercaseString<Aci>)
            case e164(e164: String)
        }

        let author: Author
        let sentTimestamp: UInt64

        init(_ addressableMessage: AddressableMessage) {
            switch addressableMessage.author {
            case .aci(let aci):
                author = .aci(aci: ServiceIdUppercaseString(wrappedValue: aci))
            case .e164(let e164):
                author = .e164(e164: e164.stringValue)
            }
            sentTimestamp = addressableMessage.sentTimestamp
        }

        var asProto: SSKProtoAddressableMessage {
            let protoBuilder = SSKProtoAddressableMessage.builder()
            protoBuilder.setSentTimestamp(sentTimestamp)
            switch author {
            case .aci(let aci):
                protoBuilder.setAuthorServiceIDBinary(aci.wrappedValue.serviceIdBinary)
            case .e164(let e164): protoBuilder.setAuthorE164(e164)
            }
            return protoBuilder.buildInfallibly()
        }
    }

    struct MessageDeletes: Codable, Equatable {
        let conversationIdentifier: CodableConversationIdentifier
        let addressableMessages: [CodableAddressableMessage]

        init(
            conversationIdentifier: ConversationIdentifier,
            addressableMessages: [AddressableMessage],
        ) {
            self.conversationIdentifier = CodableConversationIdentifier(conversationIdentifier)
            self.addressableMessages = addressableMessages.map { CodableAddressableMessage($0) }
        }

        fileprivate var asProto: SSKProtoSyncMessageDeleteForMeMessageDeletes {
            let protoBuilder = SSKProtoSyncMessageDeleteForMeMessageDeletes.builder()
            protoBuilder.setConversation(conversationIdentifier.asProto)
            protoBuilder.setMessages(addressableMessages.map { $0.asProto })
            return protoBuilder.buildInfallibly()
        }
    }

    struct AttachmentDelete: Codable, Equatable {
        let conversationIdentifier: CodableConversationIdentifier
        let targetMessage: CodableAddressableMessage
        let clientUuid: UUID?
        let encryptedDigest: Data?
        let plaintextHash: Data?

        init(
            conversationIdentifier: ConversationIdentifier,
            targetMessage: AddressableMessage,
            clientUuid: UUID?,
            encryptedDigest: Data?,
            plaintextHash: Data?,
        ) {
            self.conversationIdentifier = CodableConversationIdentifier(conversationIdentifier)
            self.targetMessage = CodableAddressableMessage(targetMessage)
            self.clientUuid = clientUuid
            self.encryptedDigest = encryptedDigest
            self.plaintextHash = plaintextHash
        }

        fileprivate var asProto: SSKProtoSyncMessageDeleteForMeAttachmentDelete {
            let protoBuilder = SSKProtoSyncMessageDeleteForMeAttachmentDelete.builder()
            protoBuilder.setConversation(conversationIdentifier.asProto)
            protoBuilder.setTargetMessage(targetMessage.asProto)
            if let clientUuid {
                protoBuilder.setClientUuid(clientUuid.data)
            }
            if let encryptedDigest {
                protoBuilder.setFallbackDigest(encryptedDigest)
            }
            if let plaintextHash {
                protoBuilder.setFallbackPlaintextHash(plaintextHash)
            }
            return protoBuilder.buildInfallibly()
        }
    }

    struct ConversationDelete: Codable, Equatable {
        private enum CodingKeys: String, CodingKey {
            case conversationIdentifier
            case mostRecentAddressableMessages
            case mostRecentNonExpiringAddressableMessages
            case isFullDelete
        }

        let conversationIdentifier: CodableConversationIdentifier
        let mostRecentAddressableMessages: [CodableAddressableMessage]
        /// Non-expiring messages were added after this type may already have
        /// been persisted, so this needs to be an optional property that
        /// decodes as `nil` from existing data.
        let mostRecentNonExpiringAddressableMessages: [CodableAddressableMessage]?
        let isFullDelete: Bool

#if TESTABLE_BUILD
        init(
            conversationIdentifier: ConversationIdentifier,
            mostRecentAddressableMessages: [AddressableMessage],
            nilNonExpiringAddressableMessages: Void,
            isFullDelete: Bool,
        ) {
            self.conversationIdentifier = CodableConversationIdentifier(conversationIdentifier)
            self.mostRecentAddressableMessages = mostRecentAddressableMessages.map { CodableAddressableMessage($0) }
            self.mostRecentNonExpiringAddressableMessages = nil
            self.isFullDelete = isFullDelete
        }
#endif

        init(
            conversationIdentifier: ConversationIdentifier,
            mostRecentAddressableMessages: [AddressableMessage],
            mostRecentNonExpiringAddressableMessages: [AddressableMessage],
            isFullDelete: Bool,
        ) {
            self.conversationIdentifier = CodableConversationIdentifier(conversationIdentifier)
            self.mostRecentAddressableMessages = mostRecentAddressableMessages.map { CodableAddressableMessage($0) }
            self.mostRecentNonExpiringAddressableMessages = mostRecentNonExpiringAddressableMessages.map { CodableAddressableMessage($0) }
            self.isFullDelete = isFullDelete
        }

        fileprivate var asProto: SSKProtoSyncMessageDeleteForMeConversationDelete {
            let protoBuilder = SSKProtoSyncMessageDeleteForMeConversationDelete.builder()
            protoBuilder.setConversation(conversationIdentifier.asProto)
            protoBuilder.setMostRecentMessages(mostRecentAddressableMessages.map { $0.asProto })
            if let mostRecentNonExpiringAddressableMessages {
                protoBuilder.setMostRecentNonExpiringMessages(mostRecentNonExpiringAddressableMessages.map { $0.asProto })
            }
            protoBuilder.setIsFullDelete(isFullDelete)
            return protoBuilder.buildInfallibly()
        }
    }

    struct LocalOnlyConversationDelete: Codable, Equatable {
        let conversationIdentifier: CodableConversationIdentifier

        init(conversationIdentifier: ConversationIdentifier) {
            self.conversationIdentifier = CodableConversationIdentifier(conversationIdentifier)
        }

        fileprivate var asProto: SSKProtoSyncMessageDeleteForMeLocalOnlyConversationDelete {
            let protoBuilder = SSKProtoSyncMessageDeleteForMeLocalOnlyConversationDelete.builder()
            protoBuilder.setConversation(conversationIdentifier.asProto)
            return protoBuilder.buildInfallibly()
        }
    }
}