Signal-iOS/SignalServiceKit/Contacts/RecipientDatabaseTable.swift
2026-01-06 13:34:35 -06:00

156 lines
6.3 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// 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 isnt 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)
}
}
}