TM-SGNL-iOS/SignalServiceKit/Contacts/UniqueObjectRecipientMerger.swift
TeleMessage developers dde0620daf initial commit
2025-05-03 12:28:28 -07:00

92 lines
4.2 KiB
Swift

//
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import Foundation
import LibSignalClient
enum UniqueRecipientObjectMerger {
/// Helps merge database objects that have one instance per recipient.
///
/// For example, we expect at most one thread per recipient and one user
/// profile per recipient, so they use this class. We expect many messages
/// and receipts for each recipient, so they don't use this method.
///
/// This method will find and return all objects that belong to `recipient`.
/// For example, if you have separate threads for a recipient's ACI and
/// phone number, both will be returned. The caller should merge all the
/// returned objects into a single object.
///
/// This method should be called after adding a new identifier to
/// `recipient` because there may be separate objects that should now be
/// merged. However, it's safe to call this method (and run the merging
/// logic) at any time in the case of bugs or other issues.
///
/// Some objects that match one of `recipient`'s identifiers may not belong
/// to `recipient`. For example, if an object has some other ACI but
/// `recipient`'s phone number, it actually belongs to the ACI's recipient.
/// In these cases, the identifier is removed from the object, and it's not
/// returned from this method.
static func fetchAndExpunge<T>(
for recipient: SignalRecipient,
serviceIdField: ReferenceWritableKeyPath<T, String?>,
phoneNumberField: ReferenceWritableKeyPath<T, String?>,
uniqueIdField: KeyPath<T, String>,
fetchObjectsForServiceId: (ServiceId) -> [T],
fetchObjectsForPhoneNumber: (E164) -> [T],
updateObject: (T) -> Void
) -> [T] {
var results = [T]()
let aci: Aci? = recipient.aci
let phoneNumber: E164? = E164(recipient.phoneNumber?.stringValue)
let pni: Pni? = recipient.pni
// Find any objects already associated with the ACI. These definitely
// belong to this account, and we'll pick an arbitrary one to be the winner
// if there's multiple matches.
if let aci {
results.append(contentsOf: fetchObjectsForServiceId(aci))
}
// Find objects associated with the phone number and merge or expunge them.
if let phoneNumber {
for object in fetchObjectsForPhoneNumber(phoneNumber) {
let serviceId = object[keyPath: serviceIdField].flatMap({ try? ServiceId.parseFrom(serviceIdString: $0) })
switch serviceId?.concreteType {
case .aci(aci), .pni, .none:
// This object already matches the ACI, has *any* PNI, or has no ACI/PNI.
// In all of these cases, we can claim it based on the phone number match.
results.append(object)
case .aci:
// This object is associated with some other ACI; expunge its phone number
// because we know it's out of date.
Logger.info("Expunging out-of-date phone number from \(type(of: object))")
object[keyPath: phoneNumberField] = nil
updateObject(object)
}
}
}
// Find any objects associated with the PNI and merge or expunge them.
if let pni {
for object in fetchObjectsForServiceId(pni) {
switch object[keyPath: phoneNumberField] {
case .some(phoneNumber?.stringValue), .none:
// This object matches the phone number or doesn't have one. We can claim
// it for this account.
results.append(object)
case .some:
// This object is associated with some other phone number; expunge its PNI
// because we know it's out of date.
Logger.info("Expunging out-of-date PNI from \(type(of: object))")
object[keyPath: serviceIdField] = nil
updateObject(object)
}
}
}
return results.removingDuplicates(uniquingElementsBy: { $0[keyPath: uniqueIdField] })
}
}