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

import LibSignalClient

/// Responsible for deleting ``CallRecord``s.
///
/// We want to take special steps when a ``CallRecord`` is deleted, beyond
/// simply removing it on-disk. This manager is responsible for performing all
/// the necessary additional tasks related to deleting a ``CallRecord``.
///
/// - SeeAlso ``DeletedCallRecord``
/// - SeeAlso ``DeletedCallRecordExpirationJob``
/// - SeeAlso ``CallRecordStore/delete(callRecords:tx:)``
public protocol CallRecordDeleteManager {
    /// Delete the given call record.
    ///
    /// - Important
    /// Callers must ensure the ``TSInteraction`` associated with the given call
    /// record is also deleted. If you're not sure if that's happening, you may
    /// want ``InteractionDeleteManager/delete(alongsideAssociatedCallRecords:associatedCallDeleteBehavior:tx:)``.
    ///
    /// - Parameter sendSyncMessageOnDelete
    /// Whether we should send an ``OutgoingCallEventSyncMessage`` if we delete
    /// a call record.
    func deleteCallRecords(
        _ callRecords: [CallRecord],
        sendSyncMessageOnDelete: Bool,
        tx: DBWriteTransaction,
    )

    /// Mark the call with the given identifiers, for which we do not have a
    /// local ``CallRecord``, as deleted.
    ///
    /// - Important
    /// This method should only be used if the caller knows a ``CallRecord``
    /// does not exist for the given identifiers.
    ///
    /// - Note
    /// Because there is no ``CallRecord``, there is no associated interaction.
    ///
    /// - Note
    /// This API never sends an ``OutgoingCallEventSyncMessage`` about the
    /// delete, as it isn't actually deleting a call this device knows about.
    func markCallAsDeleted(
        callId: UInt64,
        conversationId: CallRecord.ConversationID,
        tx: DBWriteTransaction,
    )
}

// MARK: -

final class CallRecordDeleteManagerImpl: CallRecordDeleteManager {
    private let callRecordStore: CallRecordStore
    private let outgoingCallEventSyncMessageManager: OutgoingCallEventSyncMessageManager
    private let deletedCallRecordExpirationJob: DeletedCallRecordExpirationJob
    private let deletedCallRecordStore: DeletedCallRecordStore
    private let threadStore: ThreadStore

    init(
        callRecordStore: CallRecordStore,
        outgoingCallEventSyncMessageManager: OutgoingCallEventSyncMessageManager,
        deletedCallRecordExpirationJob: DeletedCallRecordExpirationJob,
        deletedCallRecordStore: DeletedCallRecordStore,
        threadStore: ThreadStore,
    ) {
        self.callRecordStore = callRecordStore
        self.outgoingCallEventSyncMessageManager = outgoingCallEventSyncMessageManager
        self.deletedCallRecordExpirationJob = deletedCallRecordExpirationJob
        self.deletedCallRecordStore = deletedCallRecordStore
        self.threadStore = threadStore
    }

    func markCallAsDeleted(
        callId: UInt64,
        conversationId: CallRecord.ConversationID,
        tx: DBWriteTransaction,
    ) {
        insertDeletedCallRecords(
            deletedCallRecords: [
                DeletedCallRecord(
                    callId: callId,
                    conversationId: conversationId,
                ),
            ],
            tx: tx,
        )
    }

    func deleteCallRecords(
        _ callRecords: [CallRecord],
        sendSyncMessageOnDelete: Bool,
        tx: DBWriteTransaction,
    ) {
        callRecordStore.delete(callRecords: callRecords, tx: tx)

        insertDeletedCallRecords(
            deletedCallRecords: callRecords.map {
                DeletedCallRecord(callRecord: $0)
            },
            tx: tx,
        )

        if sendSyncMessageOnDelete {
            for callRecord in callRecords {
                let callEventTimestamp: UInt64

                switch callRecord.callType {
                case .audioCall, .videoCall:
                    // [Calls] TODO: pass through the "call event timestamp" for 1:1 call events
                    //
                    // We currently use the timestamp of the call record when sending all
                    // sync messages related to a 1:1 call. That's not quite right – we
                    // should be using the timestamp of the event that triggered the sync
                    // message, such as the user declining.
                    //
                    // This isn't a big deal for 1:1 calls though, since all 1:1 calls have
                    // a well-defined "start time" that both participants know about: the
                    // timestamp of the call offer message. That means no one will in
                    // practice consume this timestamp for 1:1 calls, and we can get away
                    // with it for now.
                    callEventTimestamp = callRecord.callBeganTimestamp
                case .groupCall, .adHocCall:
                    callEventTimestamp = Date().ows_millisecondsSince1970
                }

                outgoingCallEventSyncMessageManager.sendSyncMessage(
                    callRecord: callRecord,
                    callEvent: .callDeleted,
                    callEventTimestamp: callEventTimestamp,
                    tx: tx,
                )
            }
        }
    }

    private func insertDeletedCallRecords(
        deletedCallRecords: [DeletedCallRecord],
        tx: DBWriteTransaction,
    ) {
        for deletedCallRecord in deletedCallRecords {
            deletedCallRecordStore.insert(deletedCallRecord: deletedCallRecord, tx: tx)
        }

        // We've added a new DeletedCallRecord to expire, so let the expiration
        // job know.
        deletedCallRecordExpirationJob.restart()
    }
}