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

import GRDB
public import LibSignalClient

/// An interface for fetching and storing usernames. Note that with the
/// exception of the local user, any stored data about an account's username is
/// only as fresh as the last time we looked it up - usernames should be
/// considered transient.
public protocol UsernameLookupManager {
    /// Fetch the most recently-known username for the given ACI, if any.
    func fetchUsername(
        forAci aci: Aci,
        transaction: DBReadTransaction,
    ) -> String?

    /// Fetch usernames for the given addresses, returning an array with a
    /// username corresponding to each passed address, if one is available.
    func fetchUsernames(
        forAddresses addresses: AnySequence<SignalServiceAddress>,
        transaction: DBReadTransaction,
    ) -> [String?]

    /// Save the given username, or lack thereof, for the given ACI.
    func saveUsername(
        _ username: String?,
        forAci aci: Aci,
        transaction: DBWriteTransaction,
    )
}

// MARK: -

class UsernameLookupManagerImpl: UsernameLookupManager {
    private let searchableNameIndexer: any SearchableNameIndexer
    private let usernameLookupRecordStore: UsernameLookupRecordStore

    init(
        searchableNameIndexer: SearchableNameIndexer,
        usernameLookupRecordStore: UsernameLookupRecordStore,
    ) {
        self.searchableNameIndexer = searchableNameIndexer
        self.usernameLookupRecordStore = usernameLookupRecordStore
    }

    // MARK: -

    func fetchUsername(
        forAci aci: Aci,
        transaction tx: DBReadTransaction,
    ) -> String? {
        return usernameLookupRecordStore.fetchOne(forAci: aci, tx: tx)?.username
    }

    func fetchUsernames(
        forAddresses addresses: some Sequence<SignalServiceAddress>,
        transaction tx: DBReadTransaction,
    ) -> [String?] {
        return addresses.map { address -> String? in
            guard let aci = address.aci else { return nil }
            return fetchUsername(forAci: aci, transaction: tx)
        }
    }

    // MARK: -

    func saveUsername(
        _ username: String?,
        forAci aci: Aci,
        transaction tx: DBWriteTransaction,
    ) {
        // First, delete any existing records for this ACI. This ensures that we
        // can, if we insert a new record, subsequently index the new username
        // in FTS via SearchableName.
        usernameLookupRecordStore.deleteOne(forAci: aci, tx: tx)

        if let username {
            // The `username` column is UNIQUE (enforced by an index), so we
            // must take care to wipe any previous associations for this
            // username (case-insensitive).
            //
            // This is semantically appropriate as usernames can only be
            // associated with one ACI at a time.
            if
                let conflictingRecord = usernameLookupRecordStore.fetchOne(
                    forUsernameCaseInsensitive: username,
                    tx: tx,
                )
            {
                let conflictingAci = Aci(fromUUID: conflictingRecord.aci)
                usernameLookupRecordStore.deleteOne(forAci: conflictingAci, tx: tx)
            }

            let usernameLookupRecord = UsernameLookupRecord(aci: aci, username: username)
            usernameLookupRecordStore.insertOne(usernameLookupRecord, tx: tx)
            searchableNameIndexer.insert(usernameLookupRecord, tx: tx)
        }
    }
}