// // Copyright 2024 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only // import Foundation import LibSignalClient public import SignalServiceKit public class SearchableNameFinder { private let contactManager: any ContactManager private let searchableNameIndexer: any SearchableNameIndexer private let phoneNumberVisibilityFetcher: any PhoneNumberVisibilityFetcher private let recipientDatabaseTable: RecipientDatabaseTable public init( contactManager: any ContactManager, searchableNameIndexer: any SearchableNameIndexer, phoneNumberVisibilityFetcher: any PhoneNumberVisibilityFetcher, recipientDatabaseTable: RecipientDatabaseTable ) { self.contactManager = contactManager self.searchableNameIndexer = searchableNameIndexer self.phoneNumberVisibilityFetcher = phoneNumberVisibilityFetcher self.recipientDatabaseTable = recipientDatabaseTable } public func searchNames( for searchText: String, maxResults: Int, localIdentifiers: LocalIdentifiers, tx: DBReadTransaction, addGroupThread: (TSGroupThread) -> Void, addStoryThread: (TSPrivateStoryThread) -> Void ) throws(CancellationError) -> [SignalServiceAddress] { var contactMatches = ContactMatches() try searchableNameIndexer.search( for: searchText, maxResults: maxResults, tx: tx ) { indexableName throws(CancellationError) in if Task.isCancelled { throw CancellationError() } switch indexableName { case let signalAccount as SignalAccount: contactMatches.addResult(for: signalAccount) case let userProfile as OWSUserProfile: contactMatches.addResult(for: userProfile, localIdentifiers: localIdentifiers) case let signalRecipient as SignalRecipient: contactMatches.addResult( for: signalRecipient, phoneNumberVisibilityFetcher: phoneNumberVisibilityFetcher, tx: tx ) case let usernameLookupRecord as UsernameLookupRecord: contactMatches.addResult(for: usernameLookupRecord) case let nicknameRecord as NicknameRecord: contactMatches.addResult( for: nicknameRecord, recipientDatabaseTable: self.recipientDatabaseTable, tx: tx ) case let groupThread as TSGroupThread: addGroupThread(groupThread) case let storyThread as TSPrivateStoryThread: addStoryThread(storyThread) case is TSContactThread: break default: owsFailDebug("Unexpected match of type \(type(of: indexableName))") } } return contactMatches.matchedAddresses(contactManager: contactManager, tx: SDSDB.shimOnlyBridge(tx)) } } private struct ContactMatches { private struct ContactMatch { var nickname: NicknameRecord? var signalAccount: SignalAccount? var userProfile: OWSUserProfile? var signalRecipient: SignalRecipient? var usernameLookupRecord: UsernameLookupRecord? } private var rawValue = [SignalServiceAddress: ContactMatch]() public var count: Int { rawValue.count } mutating func addResult( for nickname: NicknameRecord, recipientDatabaseTable: RecipientDatabaseTable, tx: DBReadTransaction ) { guard let recipient = recipientDatabaseTable.fetchRecipient(rowId: nickname.recipientRowID, tx: tx) else { return } let address = recipient.address withUnsafeMutablePointer(to: &rawValue[address, default: ContactMatch()]) { $0.pointee.nickname = nickname } } mutating func addResult(for signalAccount: SignalAccount) { let address = signalAccount.recipientAddress withUnsafeMutablePointer(to: &rawValue[address, default: ContactMatch()]) { $0.pointee.signalAccount = signalAccount } } mutating func addResult(for userProfile: OWSUserProfile, localIdentifiers: LocalIdentifiers) { let address = userProfile.publicAddress(localIdentifiers: localIdentifiers) withUnsafeMutablePointer(to: &rawValue[address, default: ContactMatch()]) { $0.pointee.userProfile = userProfile } } mutating func addResult( for signalRecipient: SignalRecipient, phoneNumberVisibilityFetcher: any PhoneNumberVisibilityFetcher, tx: DBReadTransaction ) { guard signalRecipient.isRegistered else { return } guard phoneNumberVisibilityFetcher.isPhoneNumberVisible(for: signalRecipient, tx: tx) else { return } let address = signalRecipient.address withUnsafeMutablePointer(to: &rawValue[address, default: ContactMatch()]) { $0.pointee.signalRecipient = signalRecipient } } mutating func addResult(for usernameLookupRecord: UsernameLookupRecord) { let address = SignalServiceAddress(Aci(fromUUID: usernameLookupRecord.aci)) withUnsafeMutablePointer(to: &rawValue[address, default: ContactMatch()]) { $0.pointee.usernameLookupRecord = usernameLookupRecord } } mutating func removeResult(for address: SignalServiceAddress) { rawValue.removeValue(forKey: address) } func matchedAddresses(contactManager: any ContactManager, tx: DBReadTransaction) -> [SignalServiceAddress] { var results = [SignalServiceAddress]() for (address, contactMatch) in rawValue { let displayName = contactManager.displayName(for: address, tx: tx) let isValidName: Bool switch displayName { case .nickname: isValidName = contactMatch.nickname != nil case .systemContactName: isValidName = contactMatch.signalAccount != nil case .profileName: isValidName = contactMatch.userProfile != nil case .username: isValidName = contactMatch.usernameLookupRecord != nil case .phoneNumber, .deletedAccount, .unknown: isValidName = false } if isValidName || contactMatch.signalRecipient != nil { results.append(address) } } return results } }