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

import Foundation
import LibSignalClient

@objc(OWSSyncMessageRequestResponseMessage)
public final class OutgoingMessageRequestResponseSyncMessage: OutgoingSyncMessage {

    public enum ResponseType: UInt64 {
        case accept = 0
        case delete = 1
        case block = 2
        case blockAndDelete = 3
        case spam = 4
        case blockAndSpam = 5

        fileprivate var asProtoResponseType: SSKProtoSyncMessageMessageRequestResponseType {
            switch self {
            case .accept: .accept
            case .delete: .delete
            case .block: .block
            case .blockAndDelete: .blockAndDelete
            case .spam: .spam
            case .blockAndSpam: .blockAndSpam
            }
        }
    }

    // v0: The sending thread is also the acted-upon thread.
    // v1: (skipped to avoid ambiguity)
    // v2: The acted-upon thread is stored in groupId/threadAci.
    let version: UInt

    let groupId: Data?
    let threadAci: Aci?
    let responseType: ResponseType

    override public class var supportsSecureCoding: Bool { true }

    override public func encode(with coder: NSCoder) {
        super.encode(with: coder)
        if let groupId {
            coder.encode(groupId, forKey: "groupId")
        }
        coder.encode(NSNumber(value: self.responseType.rawValue), forKey: "responseType")
        if let threadAci {
            coder.encode(threadAci.serviceIdString, forKey: "threadAci")
        }
        coder.encode(NSNumber(value: self.version), forKey: "version")
    }

    public required init?(coder: NSCoder) {
        guard
            let rawResponseType = coder.decodeObject(of: NSNumber.self, forKey: "responseType"),
            let responseType = ResponseType(rawValue: rawResponseType.uint64Value)
        else {
            return nil
        }
        self.responseType = responseType
        self.groupId = coder.decodeObject(of: NSData.self, forKey: "groupId") as Data?
        let threadAciString = coder.decodeObject(of: NSString.self, forKey: "threadAci") as String?
        if let threadAciString {
            guard let threadAci = Aci.parseFrom(aciString: threadAciString) else {
                return nil
            }
            self.threadAci = threadAci
        } else {
            self.threadAci = nil
        }
        self.version = coder.decodeObject(of: NSNumber.self, forKey: "version")?.uintValue ?? 0
        super.init(coder: coder)
    }

    override public var hash: Int {
        var hasher = Hasher()
        hasher.combine(super.hash)
        hasher.combine(self.groupId)
        hasher.combine(self.responseType)
        hasher.combine(self.threadAci)
        hasher.combine(self.version)
        return hasher.finalize()
    }

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

    init(
        localThread: TSContactThread,
        messageRequestThread: TSThread,
        responseType: ResponseType,
        tx: DBReadTransaction,
    ) {
        self.version = 2
        switch messageRequestThread {
        case let thread as TSGroupThread:
            self.groupId = thread.groupId
            self.threadAci = nil
        case let thread as TSContactThread:
            self.groupId = nil
            self.threadAci = thread.contactAddress.aci
            owsAssertDebug(self.threadAci != nil, "must have ACI when responding to a message request")
        default:
            self.groupId = nil
            self.threadAci = nil
            owsFailDebug("can't response to thread type")
        }
        self.responseType = responseType
        super.init(localThread: localThread, tx: tx)
    }

    override public func syncMessageBuilder(tx: DBReadTransaction) -> SSKProtoSyncMessageBuilder? {
        let messageRequestResponseBuilder = SSKProtoSyncMessageMessageRequestResponse.builder()
        messageRequestResponseBuilder.setType(self.responseType.asProtoResponseType)

        if let groupId {
            messageRequestResponseBuilder.setGroupID(groupId)
        } else if let threadAci {
            messageRequestResponseBuilder.setThreadAciBinary(threadAci.serviceIdBinary)
        } else if self.version < 2 {
            // Fallback behavior. Messages of this version are no longer created.
            // Eventually, all enqueued messages of this type should be resolved
            // (either because they have been sent or because they ran out of retries).
            let thread = self.thread(tx: tx)
            guard let thread else {
                owsFailDebug("Missing thread for message request response")
                return nil
            }

            switch thread {
            case let thread as TSGroupThread:
                messageRequestResponseBuilder.setGroupID(thread.groupModel.groupId)
            case let thread as TSContactThread:
                if let threadAci = thread.contactAddress.serviceId as? Aci {
                    messageRequestResponseBuilder.setThreadAciBinary(threadAci.serviceIdBinary)
                }
            default:
                owsFailDebug("Thread is invalid type for message request response")
                return nil
            }
        }

        let builder = SSKProtoSyncMessage.builder()
        builder.setMessageRequestResponse(messageRequestResponseBuilder.buildInfallibly())
        return builder
    }

    override public var isUrgent: Bool { false }
}