Path: blob/main/SignalServiceKit/Contacts/RecipientDatabaseTable.swift
1 views
//
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import Foundation
import GRDB
public import LibSignalClient
public struct RecipientDatabaseTable {
public init() {}
func fetchRecipient(contactThread: TSContactThread, tx: DBReadTransaction) -> SignalRecipient? {
return fetchServiceIdAndRecipient(contactThread: contactThread, tx: tx)
.flatMap { _, recipient in recipient }
}
func fetchServiceId(contactThread: TSContactThread, tx: DBReadTransaction) -> ServiceId? {
return fetchServiceIdAndRecipient(contactThread: contactThread, tx: tx)
.map { serviceId, _ in serviceId }
}
/// Fetch the `ServiceId` for the owner of this contact thread, and its
/// corresponding `SignalRecipient` if one exists.
private func fetchServiceIdAndRecipient(
contactThread: TSContactThread,
tx: DBReadTransaction,
) -> (ServiceId, SignalRecipient?)? {
let threadServiceId = contactThread.contactUUID.flatMap { try? ServiceId.parseFrom(serviceIdString: $0) }
// If there's an ACI, it's *definitely* correct, and it's definitely the
// owner, so we can return early without issuing any queries.
if let aci = threadServiceId as? Aci {
let ownedByRecipient = fetchRecipient(serviceId: aci, transaction: tx)
return (aci, ownedByRecipient)
}
// Otherwise, we need to figure out which recipient "owns" this thread. If
// the thread has a phone number but there's no SignalRecipient with that
// phone number, we'll return nil (even if the thread has a PNI). This is
// intentional. In this case, the phone number takes precedence, and this
// PNI definitely isn’t associated with this phone number. This situation
// should be impossible because ThreadMerger should keep these values in
// sync. (It's pre-ThreadMerger threads that might be wrong, and PNIs were
// introduced after ThreadMerger.)
if let phoneNumber = contactThread.contactPhoneNumber {
let ownedByRecipient = fetchRecipient(phoneNumber: phoneNumber, transaction: tx)
let ownedByServiceId = ownedByRecipient?.aci ?? ownedByRecipient?.pni
return ownedByServiceId.map { ($0, ownedByRecipient) }
}
if let pni = threadServiceId as? Pni {
let ownedByRecipient = fetchRecipient(serviceId: pni, transaction: tx)
let ownedByServiceId = ownedByRecipient?.aci ?? ownedByRecipient?.pni ?? pni
return (ownedByServiceId, ownedByRecipient)
}
return nil
}
// MARK: -
public func fetchRecipient(address: SignalServiceAddress, tx: DBReadTransaction) -> SignalRecipient? {
return
address.serviceId.flatMap({ fetchRecipient(serviceId: $0, transaction: tx) })
?? address.phoneNumber.flatMap({ fetchRecipient(phoneNumber: $0, transaction: tx) })
}
public func fetchAuthorRecipient(incomingMessage: TSIncomingMessage, tx: DBReadTransaction) -> SignalRecipient? {
return fetchRecipient(address: incomingMessage.authorAddress, tx: tx)
}
public func fetchRecipient(rowId: Int64, tx: DBReadTransaction) -> SignalRecipient? {
return failIfThrows {
return try SignalRecipient.fetchOne(tx.database, key: rowId)
}
}
public func fetchRecipient(uniqueId: String, tx: DBReadTransaction) -> SignalRecipient? {
let sql = "SELECT * FROM \(SignalRecipient.databaseTableName) WHERE \(signalRecipientColumn: .uniqueId) = ?"
return failIfThrows {
return try SignalRecipient.fetchOne(tx.database, sql: sql, arguments: [uniqueId])
}
}
public func fetchRecipient(serviceId: ServiceId, transaction tx: DBReadTransaction) -> SignalRecipient? {
let serviceIdColumn: SignalRecipient.CodingKeys = {
switch serviceId.kind {
case .aci: return .aciString
case .pni: return .pni
}
}()
let sql = "SELECT * FROM \(SignalRecipient.databaseTableName) WHERE \(signalRecipientColumn: serviceIdColumn) = ?"
return failIfThrows {
return try SignalRecipient.fetchOne(tx.database, sql: sql, arguments: [serviceId.serviceIdUppercaseString])
}
}
public func fetchRecipient(phoneNumber: String, transaction tx: DBReadTransaction) -> SignalRecipient? {
let sql = "SELECT * FROM \(SignalRecipient.databaseTableName) WHERE \(signalRecipientColumn: .phoneNumber) = ?"
return failIfThrows {
return try SignalRecipient.fetchOne(tx.database, sql: sql, arguments: [phoneNumber])
}
}
public func enumerateAll(tx: DBReadTransaction, block: (SignalRecipient) -> Void) {
failIfThrows {
let cursor = try SignalRecipient.fetchCursor(tx.database)
var hasMore = true
while hasMore {
try autoreleasepool {
guard let recipient = try cursor.next() else {
hasMore = false
return
}
block(recipient)
}
}
}
}
public func fetchWhitelistedRecipients(tx: DBReadTransaction) -> [SignalRecipient] {
let fetchRequest = SignalRecipient.filter(
Column(SignalRecipient.CodingKeys.status.rawValue) == SignalRecipient.Status.whitelisted.rawValue,
)
return failIfThrows { try fetchRequest.fetchAll(tx.database) }
}
public func fetchAllPhoneNumbers(tx: DBReadTransaction) -> [String: Bool] {
var result = [String: Bool]()
enumerateAll(tx: tx) { signalRecipient in
guard let phoneNumber = signalRecipient.phoneNumber?.stringValue else {
return
}
result[phoneNumber] = signalRecipient.isRegistered
}
return result
}
public func updateRecipient(_ signalRecipient: SignalRecipient, transaction: DBWriteTransaction) {
failIfThrows {
try signalRecipient.update(transaction.database)
}
}
public func removeRecipient(_ signalRecipient: SignalRecipient, transaction: DBWriteTransaction) {
failIfThrows {
try signalRecipient.delete(transaction.database)
}
}
}