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

import Foundation
public import LibSignalClient

/// A message sent to the other participants in a call to pass along a RingRTC
/// payload out-of-band.
///
/// Not to be confused with a ``TSCall``.
public final class OutgoingCallMessage: TransientOutgoingMessage {
    public enum MessageType: Hashable {
        case offerMessage(SSKProtoCallMessageOffer)
        case answerMessage(SSKProtoCallMessageAnswer)
        case iceUpdateMessages([SSKProtoCallMessageIceUpdate])
        case hangupMessage(SSKProtoCallMessageHangup)
        case busyMessage(SSKProtoCallMessageBusy)
        case opaqueMessage(SSKProtoCallMessageOpaque)
    }

    let messageType: MessageType
    private let destinationDeviceId: UInt32?

    public init(
        thread: TSThread,
        messageType: MessageType,
        destinationDeviceId: UInt32? = nil,
        overrideRecipients: [Aci] = [],
        tx: DBReadTransaction,
    ) {
        self.messageType = messageType
        self.destinationDeviceId = destinationDeviceId
        super.init(
            outgoingMessageWith: TSOutgoingMessageBuilder.outgoingMessageBuilder(thread: thread),
            additionalRecipients: [],
            explicitRecipients: overrideRecipients.map(AciObjC.init(_:)),
            skippedRecipients: [],
            transaction: tx,
        )
    }

    override public class var supportsSecureCoding: Bool { true }

    override public func encode(with coder: NSCoder) {
        owsFail("Doesn't support serialization.")
    }

    public required init?(coder: NSCoder) {
        // Doesn't support serialization.
        return nil
    }

    override public var hash: Int {
        var hasher = Hasher()
        hasher.combine(super.hash)
        hasher.combine(self.messageType)
        hasher.combine(self.destinationDeviceId)
        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.messageType == object.messageType else { return false }
        guard self.destinationDeviceId == object.destinationDeviceId else { return false }
        return true
    }

    override public func shouldSyncTranscript() -> Bool { false }

    override public func contentBuilder(thread: TSThread, transaction: DBReadTransaction) -> SSKProtoContentBuilder? {
        let builder = SSKProtoCallMessage.builder()

        var shouldHaveProfileKey = false

        switch messageType {
        case .offerMessage(let offerMessage):
            builder.setOffer(offerMessage)
            shouldHaveProfileKey = true
        case .answerMessage(let answerMessage):
            builder.setAnswer(answerMessage)
            shouldHaveProfileKey = true
        case .iceUpdateMessages(let iceUpdateMessages):
            builder.setIceUpdate(iceUpdateMessages)
        case .hangupMessage(let hangupMessage):
            builder.setHangup(hangupMessage)
        case .busyMessage(let busyMessage):
            builder.setBusy(busyMessage)
        case .opaqueMessage(let opaqueMessage):
            builder.setOpaque(opaqueMessage)
        }

        if let destinationDeviceId {
            builder.setDestinationDeviceID(destinationDeviceId)
        }

        if shouldHaveProfileKey {
            ProtoUtils.addLocalProfileKeyIfNecessary(thread, callMessageBuilder: builder, transaction: transaction)
        }

        do {
            let contentBuilder = SSKProtoContent.builder()
            contentBuilder.setCallMessage(try builder.build())
            return contentBuilder
        } catch {
            owsFailDebug("couldn't build call message: \(error)")
            return nil
        }
    }

    override public var isUrgent: Bool {
        switch self.messageType {
        case .offerMessage:
            return true
        case .opaqueMessage(let opaqueMessage):
            switch opaqueMessage.urgency {
            case .handleImmediately:
                return true
            case .droppable, nil:
                break
            }
        default:
            break
        }
        return false
    }

    override public var debugDescription: String {
        let payloadType: String
        switch messageType {
        case .offerMessage:
            payloadType = "offerMessage"
        case .answerMessage:
            payloadType = "answerMessage"
        case .iceUpdateMessages(let iceUpdateMessages):
            payloadType = "iceUpdateMessages: \(iceUpdateMessages.count)"
        case .hangupMessage:
            payloadType = "hangupMessage"
        case .busyMessage:
            payloadType = "busyMessage"
        case .opaqueMessage:
            payloadType = "opaqueMessage"
        }
        return "\(type(of: self)) with payload: \(payloadType)"
    }

    override public var shouldRecordSendLog: Bool { false }

    override public var contentHint: SealedSenderContentHint { .default }
}