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

public extension BackupArchive {

    /// The ringRTC-provided call id for a call, shared across participating clients
    /// and persisted to the backup.
    /// Uniquely identifies an Ad-hoc Call in both the app and the backup.
    /// Representations of past non-ad-hoc calls also have call ids, but have
    /// alternative identifiers because they are represented as ChatItems.
    struct CallId: BackupArchive.LoggableId {
        let value: UInt64

        init(callRecord: CallRecord) {
            self.value = callRecord.callId
        }

        init(adHocCall: BackupProto_AdHocCall) {
            self.value = adHocCall.callID
        }

        public var typeLogString: String { "CallRecord" }
        public var idLogString: String { String(value) }
    }

    // We use the same identifier (the CallId from RingRTC) to identify
    // ad-hoc calls both in the running app and in the backup proto.
    typealias AdHocCallAppId = CallId
    typealias AdHocCallId = CallId
}

// MARK: -

public class BackupArchiveAdHocCallArchiver: BackupArchiveProtoStreamWriter {
    typealias AdHocCallAppId = BackupArchive.AdHocCallAppId
    typealias AdHocCallId = BackupArchive.AdHocCallId
    typealias ArchiveMultiFrameResult = BackupArchive.ArchiveMultiFrameResult<AdHocCallAppId>
    typealias ArchiveFrameError = BackupArchive.ArchiveFrameError<AdHocCallAppId>
    typealias RestoreFrameResult = BackupArchive.RestoreFrameResult<AdHocCallId>
    typealias RestoreFrameError = BackupArchive.RestoreFrameError<AdHocCallId>

    private let callRecordStore: CallRecordStore
    private let callLinkRecordStore: CallLinkRecordStore
    private let adHocCallRecordManager: AdHocCallRecordManager

    init(
        callRecordStore: CallRecordStore,
        callLinkRecordStore: CallLinkRecordStore,
        adHocCallRecordManager: AdHocCallRecordManager,
    ) {
        self.callRecordStore = callRecordStore
        self.callLinkRecordStore = callLinkRecordStore
        self.adHocCallRecordManager = adHocCallRecordManager
    }

    // MARK: -

    /// Archive all ``CallRecord``s (they map to ``BackupProto_AdHocCall``).
    ///
    /// - Returns: ``ArchiveMultiFrameResult.success`` if all frames were written without error, or either
    /// partial or complete failure otherwise.
    /// How to handle ``ArchiveMultiFrameResult.partialSuccess`` is up to the caller,
    /// but typically an error will be shown to the user, but the backup will be allowed to proceed.
    /// ``ArchiveMultiFrameResult.completeFailure``, on the other hand, will stop the entire backup,
    /// and should be used if some critical or category-wide failure occurs.
    func archiveAdHocCalls(
        stream: BackupArchiveProtoOutputStream,
        context: BackupArchive.ChatArchivingContext,
    ) throws(CancellationError) -> ArchiveMultiFrameResult {
        var partialErrors = [ArchiveFrameError]()
        do {
            try context.bencher.wrapEnumeration(
                callRecordStore.enumerateAdHocCallRecords(tx:block:),
                tx: context.tx,
            ) { record, frameBencher in
                try Task.checkCancellation()
                autoreleasepool {
                    let recordId = AdHocCallAppId(callRecord: record)

                    let callTimestamp = record.callBeganTimestamp
                    guard BackupArchive.Timestamps.isValid(callTimestamp) else {
                        partialErrors.append(.archiveFrameError(
                            .invalidAdHocCallTimestamp,
                            recordId,
                        ))
                        return
                    }

                    var adHocCallProto = BackupProto_AdHocCall()
                    adHocCallProto.callID = record.callId
                    adHocCallProto.callTimestamp = record.callBeganTimestamp

                    // It's a cross-client decision that `state` can only
                    // ever be `.generic` (even if the client state is
                    // actually `.joined`).
                    adHocCallProto.state = .generic

                    guard
                        let callLinkRecordId = BackupArchive.CallLinkRecordId(callRecordConversationId: record.conversationId)
                    else {
                        partialErrors.append(.archiveFrameError(
                            .adHocCallDoesNotHaveCallLinkAsConversationId,
                            recordId,
                        ))
                        return
                    }
                    guard let recipientId = context.recipientContext[.callLink(callLinkRecordId)] else {
                        partialErrors.append(.archiveFrameError(
                            .referencedRecipientIdMissing(.callLink(callLinkRecordId)),
                            recordId,
                        ))
                        return
                    }
                    adHocCallProto.recipientID = recipientId.value

                    let error = Self.writeFrameToStream(
                        stream,
                        objectId: recordId,
                        frameBencher: frameBencher,
                    ) {
                        var frame = BackupProto_Frame()
                        frame.adHocCall = adHocCallProto
                        return frame
                    }

                    if let error {
                        partialErrors.append(error)
                    }
                }
            }
        } catch let error as CancellationError {
            throw error
        } catch {
            return .completeFailure(.fatalArchiveError(.adHocCallIteratorError(error)))
        }

        if partialErrors.isEmpty {
            return .success
        } else {
            return .partialSuccess(partialErrors)
        }
    }

    // MARK: -

    /// Restore a single ``BackupProto_AdHocCall`` frame.
    ///
    /// - Returns: ``RestoreFrameResult.success`` if the frame was restored without error.
    /// How to handle ``RestoreFrameResult.failure`` is up to the caller,
    /// but typically an error will be shown to the user, but the restore will be allowed to proceed.
    func restore(
        _ adHocCall: BackupProto_AdHocCall,
        context: BackupArchive.ChatItemRestoringContext,
    ) -> RestoreFrameResult {
        var partialErrors = [BackupArchive.RestoreFrameError<AdHocCallId>]()

        let callId = AdHocCallId(adHocCall: adHocCall)

        let state: CallRecord.CallStatus.CallLinkCallStatus
        switch adHocCall.state {
        case .generic:
            state = .generic
        case .unknownState, .UNRECOGNIZED:
            state = .generic
        }

        let callLinkRecordId: BackupArchive.CallLinkRecordId
        let recipientId = adHocCall.callLinkRecipientId
        switch context.recipientContext[recipientId] {
        case .callLink(let _callLinkRecordId):
            callLinkRecordId = _callLinkRecordId
        default:
            return .failure([.restoreFrameError(
                .invalidProtoData(.recipientOfAdHocCallWasNotCallLink),
                callId,
            )])
        }
        let adHocCallRecord = CallRecord(
            callId: callId.value,
            callLinkRowId: callLinkRecordId.rowId,
            callStatus: state,
            callBeganTimestamp: adHocCall.callTimestamp,
        )

        if let callLinkRecord = context.recipientContext[callLinkRecordId] {
            do {
                var callLinkRecord = callLinkRecord
                callLinkRecord.didInsertCallRecord()
                try callLinkRecordStore.update(callLinkRecord, tx: context.tx)
            } catch {
                partialErrors.append(
                    .restoreFrameError(
                        .databaseInsertionFailed(error),
                        callId,
                    ),
                )
            }
        }

        do {
            try callRecordStore.insert(
                callRecord: adHocCallRecord,
                tx: context.tx,
            )
        } catch {
            return .failure(partialErrors + [.restoreFrameError(.databaseInsertionFailed(error), callId)])
        }

        if partialErrors.isEmpty {
            return .success
        } else {
            return .partialRestore(partialErrors)
        }
    }
}