// // Copyright 2019 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only // import GRDB import LibSignalClient @objc public class UserProfileFinder: NSObject { public func userProfile(for address: OWSUserProfile.Address, transaction: DBReadTransaction) -> OWSUserProfile? { return userProfiles(for: [address], tx: transaction)[0] } func userProfiles(for addresses: [OWSUserProfile.Address], tx: DBReadTransaction) -> [OWSUserProfile?] { let userProfiles = Refinery(addresses).refine { addresses in let serviceIds = addresses.map { address -> ServiceId? in switch address { case .localUser: return nil case .otherUser(let address): return address.serviceId } } return userProfilesFor(serviceIds: serviceIds, tx: tx) }.refine { addresses in let phoneNumbers = addresses.map { address -> String? in switch address { case .localUser: return OWSUserProfile.Constants.localProfilePhoneNumber case .otherUser(let address): return address.phoneNumber } } return userProfilesFor(phoneNumbers: phoneNumbers, tx: tx) }.values userProfiles.forEach { $0?.loadBadgeContent(tx: tx) } return userProfiles } func fetchUserProfiles(serviceId: ServiceId, tx: DBReadTransaction) -> [OWSUserProfile] { let userProfiles = userProfilesWhere( column: "\(userProfileColumn: .serviceIdString)", anyValueIn: [serviceId.serviceIdUppercaseString], tx: tx, ) userProfiles.forEach { $0.loadBadgeContent(tx: tx) } return userProfiles } func fetchUserProfiles(phoneNumber: String, tx: DBReadTransaction) -> [OWSUserProfile] { let userProfiles = userProfilesWhere( column: "\(userProfileColumn: .phoneNumber)", anyValueIn: [phoneNumber], tx: tx, ) userProfiles.forEach { $0.loadBadgeContent(tx: tx) } return userProfiles } private func userProfilesFor( serviceIds optionalServiceIds: [ServiceId?], tx: DBReadTransaction, ) -> [OWSUserProfile?] { return Refinery(optionalServiceIds) .refineNonnilKeys { (serviceIds: AnySequence) -> [OWSUserProfile?] in let profiles = userProfilesWhere( column: "\(userProfileColumn: .serviceIdString)", anyValueIn: Array(serviceIds.map { $0.serviceIdUppercaseString }), tx: tx, ) let index = Dictionary(grouping: profiles) { $0?.serviceIdString } return serviceIds.map { serviceId in let maybeArray = index[serviceId.serviceIdUppercaseString] return maybeArray?[0] } }.values } private func userProfilesFor( phoneNumbers optionalPhoneNumbers: [String?], tx: DBReadTransaction, ) -> [OWSUserProfile?] { return Refinery(optionalPhoneNumbers) .refineNonnilKeys { (phoneNumbers: AnySequence) -> [OWSUserProfile?] in let profiles = userProfilesWhere( column: "\(userProfileColumn: .phoneNumber)", anyValueIn: Array(phoneNumbers), tx: tx, ) let index = Dictionary(grouping: profiles) { $0?.phoneNumber } return phoneNumbers.map { phoneNumber in let maybeArray = index[phoneNumber] return maybeArray?[0] } }.values } private func userProfilesWhere( column: String, anyValueIn values: [String], tx: DBReadTransaction, ) -> [OWSUserProfile] { let qms = Array(repeating: "?", count: values.count).joined(separator: ", ") let sql = "SELECT * FROM \(OWSUserProfile.databaseTableName) WHERE \(column) in (\(qms))" var userProfiles = [OWSUserProfile]() OWSUserProfile.anyEnumerate(transaction: tx, sql: sql, arguments: StatementArguments(values)) { userProfile, _ in userProfiles.append(userProfile) } return userProfiles } func fetchAcisWithHiddenPhoneNumbers(tx: DBReadTransaction) throws -> [Aci] { let sql = """ SELECT \(userProfileColumn: .serviceIdString) FROM \(OWSUserProfile.databaseTableName) WHERE \(userProfileColumn: .isPhoneNumberShared) IS FALSE OR (\(userProfileColumn: .isPhoneNumberShared) IS NULL AND \(userProfileColumn: .givenName) IS NOT NULL) """ do { let serviceIdStrings = try String?.fetchAll(tx.database, sql: sql) return serviceIdStrings.compactMap(Aci.parseFrom(aciString:)) } catch { throw error.grdbErrorForLogging } } }