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

import Foundation
import LibSignalClient

public class AccountChecker {
    private let db: any DB
    private let networkManager: NetworkManager
    private let recipientFetcher: RecipientFetcher
    private let recipientManager: any SignalRecipientManager
    private let recipientMerger: any RecipientMerger
    private let recipientStore: RecipientDatabaseTable
    private let tsAccountManager: any TSAccountManager
    private let chatConnectionManager: ChatConnectionManager

    struct RateLimitError: Error, IsRetryableProvider {
        var retryAfter: TimeInterval

        /// This is a 4xx error, so it's not retryable without opting in.
        var isRetryableProvider: Bool { false }
    }

    init(
        db: any DB,
        networkManager: NetworkManager,
        recipientFetcher: RecipientFetcher,
        recipientManager: any SignalRecipientManager,
        recipientMerger: any RecipientMerger,
        recipientStore: RecipientDatabaseTable,
        tsAccountManager: any TSAccountManager,
        chatConnectionManager: ChatConnectionManager,
    ) {
        self.db = db
        self.networkManager = networkManager
        self.recipientFetcher = recipientFetcher
        self.recipientManager = recipientManager
        self.recipientMerger = recipientMerger
        self.recipientStore = recipientStore
        self.tsAccountManager = tsAccountManager
        self.chatConnectionManager = chatConnectionManager
    }

    /// Checks if an account exists for `serviceId`.
    ///
    /// If it exists, the `SignalRecipient` is marked as "registered". If it
    /// doesn't exist, the `SignalRecipient` is marked as "unregistered".
    func checkIfAccountExists(serviceId: ServiceId) async throws -> Bool {
        var exists = true
        do {
            try await chatConnectionManager.withUnauthService(.profiles) {
                exists = try await $0.accountExists(serviceId)
            }
        } catch let SignalError.rateLimitedError(retryAfter: retryAfter, message: _) {
            throw RateLimitError(retryAfter: retryAfter)
        }
        if exists {
            await db.awaitableWrite { tx in
                var recipient = recipientFetcher.fetchOrCreate(serviceId: serviceId, tx: tx)
                recipientManager.markAsRegisteredAndSave(&recipient, shouldUpdateStorageService: true, tx: tx)
            }
        } else {
            await db.awaitableWrite { tx in
                self.markAsUnregisteredAndSplitRecipientIfNeeded(serviceId: serviceId, shouldUpdateStorageService: true, tx: tx)
            }
        }
        return exists
    }

    func markAsUnregisteredAndSplitRecipientIfNeeded(
        serviceId: ServiceId,
        shouldUpdateStorageService: Bool,
        tx: DBWriteTransaction,
    ) {
        AssertNotOnMainThread()

        guard var recipient = recipientStore.fetchRecipient(serviceId: serviceId, transaction: tx) else {
            return
        }

        recipientManager.markAsUnregisteredAndSave(
            &recipient,
            unregisteredAt: .now,
            shouldUpdateStorageService: shouldUpdateStorageService,
            tx: tx,
        )

        guard let localIdentifiers = tsAccountManager.localIdentifiers(tx: tx) else {
            Logger.warn("Can't split recipient because we're not registered.")
            return
        }

        recipientMerger.splitUnregisteredRecipientIfNeeded(
            localIdentifiers: localIdentifiers,
            unregisteredRecipient: &recipient,
            tx: tx,
        )
    }
}