Signal-iOS/SignalServiceKit/src/Contacts/SignalAccountFinder.swift

103 lines
4.4 KiB
Swift

//
// Copyright 2019 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import Foundation
import GRDB
@objc
public class AnySignalAccountFinder: NSObject {
let grdbAdapter = GRDBSignalAccountFinder()
}
extension AnySignalAccountFinder {
@objc(signalAccountForAddress:transaction:)
func signalAccount(for address: SignalServiceAddress, transaction: SDSAnyReadTransaction) -> SignalAccount? {
switch transaction.readTransaction {
case .grdbRead(let transaction):
return grdbAdapter.signalAccount(for: address, transaction: transaction)
}
}
func signalAccounts(for addresses: [SignalServiceAddress],
transaction: SDSAnyReadTransaction) -> [SignalAccount?] {
switch transaction.readTransaction {
case .grdbRead(let transaction):
return grdbAdapter.signalAccounts(for: addresses, transaction: transaction)
}
}
}
@objc
class GRDBSignalAccountFinder: NSObject {
func signalAccount(for address: SignalServiceAddress, transaction: GRDBReadTransaction) -> SignalAccount? {
return signalAccounts(for: [address], transaction: transaction)[0]
}
func signalAccounts(for addresses: [SignalServiceAddress],
transaction: GRDBReadTransaction) -> [SignalAccount?] {
return Refinery<SignalServiceAddress, SignalAccount>(addresses).refine { addresses in
return signalAccountsForUUIDs(addresses.map { $0.uuid }, transaction: transaction)
}.refine { addresses in
return signalAccountsForPhoneNumbers(addresses.map { $0.phoneNumber },
transaction: transaction)
}.values
}
private func signalAccountsWhere(column: String, anyValueIn values: [String], transaction: GRDBReadTransaction) -> [SignalAccount?] {
guard !values.isEmpty else {
return []
}
let qms = Array(repeating: "?", count: values.count).joined(separator: ", ")
let sql = "SELECT * FROM \(SignalAccount.databaseTableName) WHERE \(column) in (\(qms))"
/// Why did we use `allSignalAccounts` instead of `SignalAccount.anyFetchAll`?
/// The reason is that the `SignalAccountReadCache` needs to have
/// `didReadSignalAccount` called on it for each record we enumerate, and
/// `SignalAccount.anyEnumerate` has this built in.
return allSignalAccounts(transaction: transaction, sql: sql, arguments: StatementArguments(values))
}
private func allSignalAccounts(
transaction: GRDBReadTransaction,
sql: String,
arguments: StatementArguments
) -> [SignalAccount] {
var result = [SignalAccount]()
SignalAccount.anyEnumerate(transaction: transaction.asAnyRead, sql: sql, arguments: arguments) { account, _ in
result.append(account)
}
return result
}
private func signalAccountsForUUIDs(_ uuids: [UUID?], transaction: GRDBReadTransaction) -> [SignalAccount?] {
let accounts = signalAccountsWhere(column: SignalAccount.columnName(.recipientUUID),
anyValueIn: uuids.lazy.compactMap { $0?.uuidString },
transaction: transaction)
let index: [String?: [SignalAccount?]] = Dictionary(grouping: accounts) { $0?.recipientUUID }
return uuids.map { maybeUUID -> SignalAccount? in
guard let uuid = maybeUUID else {
return nil
}
return index[uuid.uuidString]?.first ?? nil
}
}
private func signalAccountsForPhoneNumbers(_ phoneNumbers: [String?], transaction: GRDBReadTransaction) -> [SignalAccount?] {
return Refinery<String?, SignalAccount>(phoneNumbers).refineNonnilKeys { phoneNumbers -> [SignalAccount?] in
let accounts = signalAccountsWhere(column: SignalAccount.columnName(.recipientPhoneNumber),
anyValueIn: Array(phoneNumbers),
transaction: transaction)
let index = Dictionary(grouping: accounts) { $0?.recipientPhoneNumber }
let orderedAccounts = phoneNumbers.map { phoneNumber -> SignalAccount? in
guard let array = index[phoneNumber], let first = array.first else {
return nil
}
return first
}
return orderedAccounts
}.values
}
}