92 lines
4.2 KiB
Swift
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] })
|
|
}
|
|
}
|