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

import Foundation
import GRDB

/// Responsible for cleaning up expired ``DeletedCallRecord``s.
///
/// ``DeletedCallRecord``s are only intended to exist on-disk for as long as is
/// necessary to silently swallow events related to a call the user deleted.
/// Once that period has concluded – i.e., the ``DeletedCallRecord`` has
/// "expired" – this job is responsible for deleting the ``DeletedCallRecord``.
///
/// - Note
/// "Expiration time" for a ``DeletedCallRecord`` is a function of its
/// ``DeletedCallRecord/deletedAtTimestamp`` property. Consequently, the phrase
/// "multiple records with the same expiration time" is equivalent to "multiple
/// records with the same `deletedAtTimestamp`.
public final class DeletedCallRecordExpirationJob: ExpirationJob<DeletedCallRecord> {
    private let callLinkStore: any CallLinkRecordStore
    private let deletedCallRecordStore: DeletedCallRecordStore

    init(
        callLinkStore: CallLinkRecordStore,
        dateProvider: @escaping DateProvider,
        db: DB,
        deletedCallRecordStore: DeletedCallRecordStore,
    ) {
        self.callLinkStore = callLinkStore
        self.deletedCallRecordStore = deletedCallRecordStore

        super.init(
            dateProvider: dateProvider,
            db: db,
            logger: PrefixedLogger(prefix: "[DeletedCallRecordExpJob]"),
        )
    }

    // MARK: -

    override public func nextExpiringElement(tx: DBReadTransaction) -> DeletedCallRecord? {
        return deletedCallRecordStore.nextDeletedRecord(tx: tx)
    }

    override public func expirationDate(ofElement deletedCallRecord: DeletedCallRecord) -> Date {
        // A DeletedCallRecord is kept for 8h from its deletedAtTimestamp, such
        // that we can swallow call events for those 8h.
        return Date(millisecondsSince1970: deletedCallRecord.deletedAtTimestamp)
            .addingTimeInterval(8 * .hour)
    }

    override public func deleteExpiredElement(_ deletedCallRecord: DeletedCallRecord, tx: DBWriteTransaction) {
        deletedCallRecordStore.delete(
            expiredDeletedCallRecord: deletedCallRecord,
            tx: tx,
        )

        do {
            try deleteCallLinkIfNeeded(conversationId: deletedCallRecord.conversationId, tx: tx)
        } catch {
            owsFailDebug("\(error)")
        }
    }

    /// Removes the ``CallLinkRecord`` if there are no more references.
    private func deleteCallLinkIfNeeded(conversationId: CallRecord.ConversationID, tx: DBWriteTransaction) throws {
        let callLinkRowId: Int64
        switch conversationId {
        case .thread:
            return
        case .callLink(let callLinkRowId2):
            callLinkRowId = callLinkRowId2
        }
        let callLinkRecord = try callLinkStore.fetch(rowId: callLinkRowId, tx: tx) ?? {
            throw OWSAssertionError("Must be able to find call link.")
        }()
        if callLinkRecord.isDeleted {
            // We can't delete this until Storage Service is done with it.
            return
        }
        do {
            try callLinkStore.delete(callLinkRecord, tx: tx)
        } catch DatabaseError.SQLITE_CONSTRAINT {
            // We'll delete it later -- something else is still using it.
        }
    }
}