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

885 lines
35 KiB
Swift

//
// Copyright 2019 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import Foundation
public import LibSignalClient
// MARK: -
@objc
public class SignalServiceAddress: NSObject, NSCopying, NSSecureCoding, Codable {
private let cachedAddress: CachedAddress
@objc
public var phoneNumber: String? {
cachedAddress.identifiers.get().phoneNumber
}
public var e164: E164? { phoneNumber.flatMap { E164($0) } }
@objc
public var e164ObjC: E164ObjC? { phoneNumber.flatMap { E164ObjC($0) } }
/// The "service id" (could be an ACI or PNI).
///
/// This value is optional since it may not be present in all cases. Some
/// examples:
///
/// * A really old recipient, who you may have message history with, may not
/// have re-registered since UUIDs were added.
///
/// * When doing "find by phone number", there will be a window of time
/// where all we know about a recipient is their e164.
public var serviceId: ServiceId? {
cachedAddress.identifiers.get().serviceId
}
@objc
public var serviceIdObjC: ServiceIdObjC? { serviceId.map { ServiceIdObjC.wrapValue($0) } }
/// Returns the `serviceId` if it's an ACI.
///
/// - Note: Call this only if you **expect** an `Aci` (or nil). If the
/// result could be a `Pni`, you shouldn't call this method.
public var aci: Aci? {
guard let result = serviceId as? Aci? else {
owsFailDebug("Expected an ACI but found something else.")
return nil
}
return result
}
@objc
public var aciString: String? { aci?.serviceIdString }
@objc
public var aciUppercaseString: String? { aci?.serviceIdUppercaseString }
@objc
public var serviceIdString: String? { serviceId?.serviceIdString }
@objc
public var serviceIdUppercaseString: String? { serviceId?.serviceIdUppercaseString }
/// Returns a canonical address from the cache.
///
/// If you initialize an address with the wrong phone number, that address
/// will keep that phone number. This method will update that phone number
/// to match what's currently in the cache (ie what's on SignalRecipient).
public func withNormalizedPhoneNumber(cache: SignalServiceAddressCache? = nil) -> SignalServiceAddress {
let identifiers = cachedAddress.identifiers.get()
return SignalServiceAddress(
serviceId: identifiers.serviceId,
// If there's no ServiceId, then we look up the phone number in the cache.
phoneNumber: (identifiers.serviceId == nil) ? identifiers.phoneNumber : nil,
cache: cache ?? SSKEnvironment.shared.signalServiceAddressCacheRef
)
}
/// Returns a source-of-truth canonicalized address.
///
/// If an address is initialized with the wrong phone number, it'll keep it
/// until the phone number for the ACI changes; this method will update it
/// immediately. If you initialize an address with a PNI, it'll keep the PNI
/// forever; this method will update it to the ACI (but only if the ACI,
/// PNI, and phone number are all known and linked to one another).
public func withNormalizedPhoneNumberAndServiceId(cache: SignalServiceAddressCache? = nil) -> SignalServiceAddress {
return withNormalizedPhoneNumber(cache: cache).withNormalizedServiceId(cache: cache)
}
private func withNormalizedServiceId(cache: SignalServiceAddressCache?) -> SignalServiceAddress {
let identifiers = cachedAddress.identifiers.get()
guard let phoneNumber = identifiers.phoneNumber, identifiers.serviceId is Pni else {
// This is a private method, and `self` is already built against `cache`.
return self
}
return SignalServiceAddress(
serviceId: nil,
phoneNumber: phoneNumber,
cache: cache ?? SSKEnvironment.shared.signalServiceAddressCacheRef
)
}
// MARK: - Initializers
/// Initializes a "legacy" address.
///
/// Legacy addresses were saved by prior versions of the application before
/// ACIs were known, so they generally contain only a phone number. They are
/// sent through a migration path that allows them to be resolved to ACIs in
/// cases where other SignalServiceAddresses can't be resolved to ACIs.
///
/// "Modern legacy addresses" (ie modern builds writing to places that may
/// also contain legacy addresses) will encode the ACI instead of the phone
/// number, thus skipping the migration path in this initializer (good!).
public convenience init(
serviceId: ServiceId?,
legacyPhoneNumber phoneNumber: String?,
cache: SignalServiceAddressCache
) {
let normalizedAddress = NormalizedDatabaseRecordAddress(
serviceId: serviceId,
phoneNumber: phoneNumber
)
self.init(
serviceId: normalizedAddress?.serviceId,
phoneNumber: normalizedAddress?.phoneNumber,
isLegacyPhoneNumber: true,
cache: cache
)
}
public static func legacyAddress(serviceId: ServiceId?, phoneNumber: String?) -> SignalServiceAddress {
return SignalServiceAddress(
serviceId: serviceId,
legacyPhoneNumber: phoneNumber,
cache: SSKEnvironment.shared.signalServiceAddressCacheRef
)
}
public static func legacyAddress(aciString: String?, phoneNumber: String?) -> SignalServiceAddress {
return SignalServiceAddress(
serviceId: Aci.parseFrom(aciString: aciString),
legacyPhoneNumber: phoneNumber,
cache: SSKEnvironment.shared.signalServiceAddressCacheRef
)
}
@objc
public static func legacyAddress(serviceIdString: String?, phoneNumber: String?) -> SignalServiceAddress {
return SignalServiceAddress(
serviceId: serviceIdString.flatMap { try? ServiceId.parseFrom(serviceIdString: $0) },
legacyPhoneNumber: phoneNumber,
cache: SSKEnvironment.shared.signalServiceAddressCacheRef
)
}
public convenience init(_ e164: E164) {
self.init(phoneNumber: e164.stringValue)
}
@objc
public convenience init(phoneNumber: String) {
self.init(serviceId: nil, phoneNumber: phoneNumber)
}
/// Initializes an address that should refer to an Aci.
///
/// - Note: Call this only if you **expect** an `Aci` in all cases. If the
/// value might be a Pni, you shouldn't call this method.
@objc
public convenience init(aciString: String) {
self.init(aciString: aciString, phoneNumber: nil)
}
/// Initializes an address that should refer to an Aci.
///
/// - Note: Call this only if you **expect** an `Aci` (or nil) in all cases.
/// If the value might be a Pni, you shouldn't call this method.
@objc
public convenience init(aciString: String?, phoneNumber: String?) {
self.init(serviceIdString: aciString, allowPni: false, phoneNumber: phoneNumber)
}
/// Initializes an address for an Aci or Pni.
@objc
public convenience init(serviceIdString: String) {
self.init(serviceIdString: serviceIdString, phoneNumber: nil)
}
/// Initializes an address for an Aci or Pni.
@objc
public convenience init(serviceIdString: String?, phoneNumber: String?) {
self.init(serviceIdString: serviceIdString, allowPni: true, phoneNumber: phoneNumber)
}
/// Initializes an address for an Aci or Pni.
///
/// - Parameter allowPni: If false, PNIs will be treated as invalid.
@objc
public convenience init(serviceIdString: String?, allowPni: Bool, phoneNumber: String?) {
self.init(
serviceId: serviceIdString.flatMap {
let serviceId = try? ServiceId.parseFrom(serviceIdString: $0)
if serviceId is Aci {
return serviceId
}
if serviceId is Pni, allowPni {
return serviceId
}
owsFailDebug("Unexpectedly initialized SignalServiceAddress with invalid serviceIdString.")
return nil
},
phoneNumber: phoneNumber
)
}
@objc
public convenience init(serviceIdObjC: ServiceIdObjC) {
self.init(serviceIdObjC.wrappedValue)
}
public convenience init(_ serviceId: ServiceId) {
self.init(serviceId: serviceId, phoneNumber: nil)
}
public convenience init(serviceId: ServiceId?, e164: E164?) {
self.init(serviceId: serviceId, phoneNumber: e164?.stringValue)
}
public convenience init(serviceId: ServiceId?, phoneNumber: String?) {
self.init(
serviceId: serviceId,
phoneNumber: phoneNumber,
isLegacyPhoneNumber: false,
cache: SSKEnvironment.shared.signalServiceAddressCacheRef
)
}
internal convenience init(from address: ProtocolAddress) {
self.init(address.serviceId)
}
public convenience init(
serviceId: ServiceId?,
phoneNumber: String?,
cache: SignalServiceAddressCache
) {
self.init(
serviceId: serviceId,
phoneNumber: phoneNumber,
isLegacyPhoneNumber: false,
cache: cache
)
}
private init(
serviceId: ServiceId?,
phoneNumber: String?,
isLegacyPhoneNumber: Bool,
cache: SignalServiceAddressCache
) {
if let phoneNumber, phoneNumber.isEmpty {
owsFailDebug("Unexpectedly initialized signal service address with invalid phone number")
}
self.cachedAddress = cache.registerAddress(
proposedIdentifiers: CachedAddress.Identifiers(
serviceId: serviceId,
phoneNumber: phoneNumber
),
isLegacyPhoneNumber: isLegacyPhoneNumber
)
super.init()
if !isValid {
owsFailDebug("Unexpectedly initialized address with no identifier")
}
}
// MARK: - Codable
private enum CodingKeys: String, CodingKey {
case backingUuid, backingPhoneNumber
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
let identifiers = cachedAddress.identifiers.get()
try container.encode(identifiers.serviceId?.serviceIdUppercaseString, forKey: .backingUuid)
// Only encode the backingPhoneNumber if we don't know the UUID
try container.encode(identifiers.serviceId != nil ? nil : identifiers.phoneNumber, forKey: .backingPhoneNumber)
}
public required convenience init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let decodedServiceId = try container.decodeIfPresent(String.self, forKey: .backingUuid).map {
return try ServiceId.parseFrom(serviceIdString: $0)
}
let decodedPhoneNumber = try container.decodeIfPresent(String.self, forKey: .backingPhoneNumber)
self.init(
serviceId: decodedServiceId,
legacyPhoneNumber: decodedPhoneNumber,
cache: SSKEnvironment.shared.signalServiceAddressCacheRef
)
}
// MARK: - NSSecureCoding
public static let supportsSecureCoding: Bool = true
public func encode(with aCoder: NSCoder) {
let identifiers = cachedAddress.identifiers.get()
aCoder.encode(identifiers.serviceId.map { serviceId -> Any in
// For now, encode Acis in a backwards-compatible manner. This can be
// changed in the future, but it will prevent downgrades.
switch serviceId {
case is Aci:
return serviceId.rawUUID
default:
return Data(serviceId.serviceIdBinary)
}
}, forKey: "backingUuid")
// Only encode the backingPhoneNumber if we don't know the UUID
aCoder.encode(identifiers.serviceId != nil ? nil : identifiers.phoneNumber, forKey: "backingPhoneNumber")
}
public convenience required init?(coder aDecoder: NSCoder) {
let decodedServiceId: ServiceId?
switch aDecoder.decodeObject(of: [NSUUID.self, NSData.self], forKey: "backingUuid") {
case nil:
decodedServiceId = nil
case let serviceIdBinary as Data:
do {
decodedServiceId = try ServiceId.parseFrom(serviceIdBinary: serviceIdBinary)
} catch {
owsFailDebug("Couldn't parse serviceIdBinary.")
return nil
}
case let deprecatedUuid as NSUUID:
decodedServiceId = Aci(fromUUID: deprecatedUuid as UUID)
default:
return nil
}
let decodedPhoneNumber = aDecoder.decodeObject(of: NSString.self, forKey: "backingPhoneNumber") as String?
self.init(
serviceId: decodedServiceId,
legacyPhoneNumber: decodedPhoneNumber,
cache: SSKEnvironment.shared.signalServiceAddressCacheRef
)
}
// MARK: -
@objc
public func copy(with zone: NSZone? = nil) -> Any { return self }
public override func isEqual(_ object: Any?) -> Bool {
guard let otherAddress = object as? SignalServiceAddress else {
return false
}
let result = isEqualToAddress(otherAddress)
if result, cachedAddress.hashValue != otherAddress.cachedAddress.hashValue {
Logger.warn("Equal addresses have different hashes: \(self), other: \(otherAddress).")
}
return result
}
@objc
public func isEqualToAddress(_ otherAddress: SignalServiceAddress?) -> Bool {
guard let otherAddress else {
return false
}
let this = cachedAddress.identifiers.get()
let other = otherAddress.cachedAddress.identifiers.get()
if let thisServiceId = this.serviceId, let otherServiceId = other.serviceId {
return thisServiceId == otherServiceId
}
if let thisPhoneNumber = this.phoneNumber, let otherPhoneNumber = other.phoneNumber {
return thisPhoneNumber == otherPhoneNumber
}
return false
}
// In order to maintain a consistent hash, we use a constant value generated
// by the cache that can be mapped back to the phone number OR the UUID.
//
// This allows us to dynamically update the backing values to maintain
// the most complete address object as we learn phone <-> UUID mapping,
// while also allowing addresses to live in hash tables.
public override var hash: Int { return cachedAddress.hashValue }
@objc
public func compare(_ otherAddress: SignalServiceAddress) -> ComparisonResult {
return stringForDisplay.compare(otherAddress.stringForDisplay)
}
// MARK: -
@objc
public var isValid: Bool {
let identifiers = cachedAddress.identifiers.get()
if identifiers.serviceId != nil {
return true
}
if let phoneNumber = identifiers.phoneNumber {
return !phoneNumber.isEmpty
}
return false
}
@objc
public var isLocalAddress: Bool {
return DependenciesBridge.shared.tsAccountManager.localIdentifiersWithMaybeSneakyTransaction?.aciAddress == self
}
@objc
public var stringForDisplay: String {
let identifiers = cachedAddress.identifiers.get()
if let phoneNumber = identifiers.phoneNumber {
return phoneNumber
} else if let serviceId = identifiers.serviceId {
return serviceId.serviceIdUppercaseString
}
owsFailDebug("unexpectedly have no backing value")
return ""
}
@objc
public var serviceIdentifier: String? {
let identifiers = cachedAddress.identifiers.get()
if let serviceId = identifiers.serviceId {
return serviceId.serviceIdUppercaseString
}
if let phoneNumber = identifiers.phoneNumber {
return phoneNumber
}
if !CurrentAppContext().isRunningTests {
owsFailDebug("phoneNumber was unexpectedly nil")
}
return nil
}
@objc
public var sortKey: String {
guard let serviceIdentifier = serviceIdentifier else {
owsFailDebug("Invalid address.")
return "Invalid"
}
return serviceIdentifier
}
@objc
override public var description: String {
let identifiers = cachedAddress.identifiers.get()
return Self.addressComponentsDescription(
uuidString: identifiers.serviceId?.logString,
phoneNumber: identifiers.phoneNumber
)
}
public static func addressComponentsDescription(uuidString: String?, phoneNumber: String?) -> String {
var splits = [String]()
if let uuid = uuidString?.nilIfEmpty {
splits.append("serviceId: " + uuid)
} else if let phoneNumber = phoneNumber?.nilIfEmpty {
splits.append("phoneNumber: " + phoneNumber)
}
return "<" + splits.joined(separator: ", ") + ">"
}
}
// MARK: -
public extension Array where Element == SignalServiceAddress {
func stableSort() -> [SignalServiceAddress] {
// Use an arbitrary sort but ensure the ordering is stable.
self.sorted { $0.sortKey < $1.sortKey }
}
}
// MARK: -
private class CachedAddress {
struct Identifiers: Equatable {
var serviceId: ServiceId?
var phoneNumber: String?
}
let hashValue: Int
let identifiers: AtomicValue<Identifiers>
init(hashValue: Int, identifiers: Identifiers) {
self.hashValue = hashValue
self.identifiers = AtomicValue(identifiers, lock: .init())
}
}
public class SignalServiceAddressCache: NSObject {
private let state = AtomicValue(CacheState(), lock: .init())
private let _phoneNumberVisibilityFetcher: PhoneNumberVisibilityFetcher?
private var phoneNumberVisibilityFetcher: PhoneNumberVisibilityFetcher {
return _phoneNumberVisibilityFetcher ?? DependenciesBridge.shared.phoneNumberVisibilityFetcher
}
private struct CacheState {
var serviceIdHashValues = [ServiceId: Int]()
var phoneNumberHashValues = [String: Int]()
var serviceIdToPhoneNumber = [ServiceId: PotentiallyVisible<String>]()
var phoneNumberToServiceIds = [String: [PotentiallyVisible<ServiceId>]]()
var serviceIdCachedAddresses = [ServiceId: [CachedAddress]]()
var phoneNumberOnlyCachedAddresses = [String: [CachedAddress]]()
var phoneNumberOnlyLegacyCachedAddresses = [String: [CachedAddress]]()
}
/// Tracks a relationship that may or may not be visible.
///
/// Hidden relationships are used to handle "legacy" address migrations.
private struct PotentiallyVisible<T: Equatable>: Equatable {
var wrappedValue: T
var isVisible: Bool
}
// TODO: Remove this initializer after fixing DependenciesBridge setup.
public override init() {
self._phoneNumberVisibilityFetcher = nil
}
public init(phoneNumberVisibilityFetcher: any PhoneNumberVisibilityFetcher) {
self._phoneNumberVisibilityFetcher = phoneNumberVisibilityFetcher
}
func warmCaches() {
owsAssertDebug(GRDBSchemaMigrator.areMigrationsComplete)
SSKEnvironment.shared.databaseStorageRef.read { tx in
let bulkFetcher: BulkPhoneNumberVisibilityFetcher?
do {
bulkFetcher = try phoneNumberVisibilityFetcher.fetchAll(tx: tx.asV2Read)
} catch {
Logger.warn("Couldn't fetch visible phone numbers. Hiding all of them…")
bulkFetcher = nil
}
SignalRecipient.anyEnumerate(transaction: tx) { recipient, _ in
updateRecipient(
recipient,
isPhoneNumberVisible: (
bulkFetcher?.isPhoneNumberVisible(for: recipient) ?? false
)
)
}
}
}
/// Updates the cache to reflect `signalRecipient`.
///
/// This method doesn't require a write transaction to function, but there's
/// an assumption throughout the application that SignalServiceAddresses
/// won't change outside of a write transaction. Therefore, this method
/// requires one to allow the compiler to help enforce this invariant.
public func updateRecipient(_ signalRecipient: SignalRecipient, tx: DBWriteTransaction) {
updateRecipient(
signalRecipient,
isPhoneNumberVisible: phoneNumberVisibilityFetcher.isPhoneNumberVisible(for: signalRecipient, tx: tx)
)
}
internal func updateRecipient(_ signalRecipient: SignalRecipient, isPhoneNumberVisible: Bool) {
updateRecipient(
aci: signalRecipient.aci,
pni: signalRecipient.pni,
phoneNumber: signalRecipient.phoneNumber?.stringValue,
isPhoneNumberVisible: isPhoneNumberVisible
)
}
private func updateRecipient(aci: Aci?, pni: Pni?, phoneNumber: String?, isPhoneNumberVisible: Bool) {
state.update { cacheState in
// This cache associates phone numbers to the other identifiers. If we
// don't have a phone number, there's nothing to associate.
//
// We never remove a phone number from one recipient without *also* adding
// it to some other recipient; therefore, we handle the transfer when that
// recipient is passed to this method. (This avoids a potential problem
// that could occur if we learn about the "delete" after the "update".)
guard let phoneNumber else {
return
}
let oldPotentiallyVisibleServiceIds: [PotentiallyVisible<ServiceId>] = (
cacheState.phoneNumberToServiceIds[phoneNumber] ?? []
)
let newPotentiallyVisibleServiceIds: [PotentiallyVisible<ServiceId>] = [
aci.map { PotentiallyVisible(wrappedValue: $0, isVisible: isPhoneNumberVisible) },
pni.map { PotentiallyVisible(wrappedValue: $0, isVisible: true) },
].compacted()
// If this phone number still points at the same ServiceIds in the same
// way, there's nothing to change.
if newPotentiallyVisibleServiceIds == oldPotentiallyVisibleServiceIds {
return
}
// Update the "phone number -> service ids" lookup table.
cacheState.phoneNumberToServiceIds[phoneNumber] = newPotentiallyVisibleServiceIds
// Update the "service id -> phone number" lookup table by clearing the
// entries for any values that were removed and updating the entries for
// any values that were added or changed.
for oldServiceId in oldPotentiallyVisibleServiceIds {
if newPotentiallyVisibleServiceIds.contains(where: { $0.wrappedValue == oldServiceId.wrappedValue }) {
continue
}
cacheState.serviceIdToPhoneNumber[oldServiceId.wrappedValue] = nil
}
for newOrUpdatedServiceId in newPotentiallyVisibleServiceIds {
let oldPhoneNumber = cacheState.serviceIdToPhoneNumber.updateValue(
PotentiallyVisible(wrappedValue: phoneNumber, isVisible: newOrUpdatedServiceId.isVisible),
forKey: newOrUpdatedServiceId.wrappedValue
)
// If this ServiceId was associated with some other phone number, we need
// to break that association.
if let oldPhoneNumber, oldPhoneNumber.wrappedValue != phoneNumber {
cacheState.phoneNumberToServiceIds[oldPhoneNumber.wrappedValue]?.removeAll(where: {
return $0.wrappedValue == newOrUpdatedServiceId.wrappedValue
})
}
// This might be the first time we're learning about this ServiceId or
// phone number. If a preferred hash value is available, make sure all
// future SignalServiceAddress instances will be able to find it.
_ = hashValue(cacheState: &cacheState, serviceId: newOrUpdatedServiceId.wrappedValue, phoneNumber: phoneNumber)
}
let oldVisibleServiceIds: [ServiceId] = oldPotentiallyVisibleServiceIds.compactMap {
return $0.isVisible ? $0.wrappedValue : nil
}
let newVisibleServiceIds: [ServiceId] = newPotentiallyVisibleServiceIds.compactMap {
return $0.isVisible ? $0.wrappedValue : nil
}
// These ServiceIds are no longer visibly associated with `phoneNumber`.
for serviceId in Set(oldVisibleServiceIds).subtracting(newVisibleServiceIds) {
cacheState.serviceIdCachedAddresses[serviceId]?.forEach { cachedAddress in
cachedAddress.identifiers.update { $0.phoneNumber = nil }
}
}
// These ServiceIds are now visibly associated with `phoneNumber`.
for serviceId in Set(newVisibleServiceIds).subtracting(oldVisibleServiceIds) {
cacheState.serviceIdCachedAddresses[serviceId]?.forEach { cachedAddress in
cachedAddress.identifiers.update { $0.phoneNumber = phoneNumber }
}
}
// "Legacy" addresses can be resolved using any ServiceId.
updatePhoneNumberOnlyAddresses(
phoneNumberOnlyCachedAddresses: &cacheState.phoneNumberOnlyLegacyCachedAddresses,
serviceIdCachedAddresses: &cacheState.serviceIdCachedAddresses,
phoneNumber: phoneNumber,
serviceId: newPotentiallyVisibleServiceIds.first
)
// Other addresses can only be resolved with a visible ServiceId.
updatePhoneNumberOnlyAddresses(
phoneNumberOnlyCachedAddresses: &cacheState.phoneNumberOnlyCachedAddresses,
serviceIdCachedAddresses: &cacheState.serviceIdCachedAddresses,
phoneNumber: phoneNumber,
serviceId: newPotentiallyVisibleServiceIds.first(where: { $0.isVisible })
)
}
}
/// If we're adding a ServiceId to this recipient for the first time, we may
/// have some addresses with only a phone number. We should add the "best"
/// ServiceId available to those addresses. Once we add a ServiceId, that
/// value is "sticky" and won't be changed if we get an even better
/// identifier in the future. This maintains the existing (very useful)
/// invariant that a ServiceId for a SignalServiceAddress remains stable.
private func updatePhoneNumberOnlyAddresses(
phoneNumberOnlyCachedAddresses: inout [String: [CachedAddress]],
serviceIdCachedAddresses: inout [ServiceId: [CachedAddress]],
phoneNumber: String,
serviceId: PotentiallyVisible<ServiceId>?
) {
guard let serviceId else {
return
}
phoneNumberOnlyCachedAddresses.removeValue(forKey: phoneNumber)?.forEach { cachedAddress in
cachedAddress.identifiers.update {
$0.serviceId = serviceId.wrappedValue
if !serviceId.isVisible { $0.phoneNumber = nil }
}
// This address has a serviceId now -- track that serviceId for future updates.
serviceIdCachedAddresses[serviceId.wrappedValue, default: []].append(cachedAddress)
}
}
fileprivate func registerAddress(proposedIdentifiers: CachedAddress.Identifiers, isLegacyPhoneNumber: Bool) -> CachedAddress {
state.update { cacheState in
let resolvedIdentifiers = resolveIdentifiers(
proposedIdentifiers,
isLegacyPhoneNumber: isLegacyPhoneNumber,
cacheState: cacheState
)
// We try our best to share hash values for ServiceIds and phone numbers
// that might be associated with one another.
let hashValue = hashValue(
cacheState: &cacheState,
serviceId: resolvedIdentifiers.serviceId,
phoneNumber: resolvedIdentifiers.phoneNumber
)
func getOrCreateCachedAddress<T>(key: T, in cachedAddresses: inout [T: [CachedAddress]]) -> CachedAddress {
for cachedAddress in cachedAddresses[key, default: []] {
if cachedAddress.hashValue == hashValue && cachedAddress.identifiers.get() == resolvedIdentifiers {
return cachedAddress
}
}
let result = CachedAddress(hashValue: hashValue, identifiers: resolvedIdentifiers)
cachedAddresses[key, default: []].append(result)
return result
}
if let serviceId = resolvedIdentifiers.serviceId {
return getOrCreateCachedAddress(key: serviceId, in: &cacheState.serviceIdCachedAddresses)
}
if let phoneNumber = resolvedIdentifiers.phoneNumber {
if isLegacyPhoneNumber {
return getOrCreateCachedAddress(key: phoneNumber, in: &cacheState.phoneNumberOnlyLegacyCachedAddresses)
} else {
return getOrCreateCachedAddress(key: phoneNumber, in: &cacheState.phoneNumberOnlyCachedAddresses)
}
}
return CachedAddress(hashValue: hashValue, identifiers: resolvedIdentifiers)
}
}
/// Populates missing/stale identifiers populated from the cache.
///
/// - Parameter isLegacyPhoneNumber: If true, phone numbers can be resolved
/// to hidden ACIs. This ensures legacy values (eg, receipts for old
/// messages) will continue to associate with the correct account. In these
/// cases, the returned identifiers won't contain the proposed phone number.
/// If false, phone numbers can't be resolved to hidden ACIs (but they can
/// be resolved to PNIs which are always visible to phone numbers).
private func resolveIdentifiers(
_ proposedIdentifiers: CachedAddress.Identifiers,
isLegacyPhoneNumber: Bool,
cacheState: CacheState
) -> CachedAddress.Identifiers {
var resolvedIdentifiers = proposedIdentifiers
resolveServiceId(
in: &resolvedIdentifiers,
isLegacyPhoneNumber: isLegacyPhoneNumber,
cacheState: cacheState
)
resolvePhoneNumber(in: &resolvedIdentifiers, cacheState: cacheState)
return resolvedIdentifiers
}
private func resolveServiceId(
in identifiers: inout CachedAddress.Identifiers,
isLegacyPhoneNumber: Bool,
cacheState: CacheState
) {
guard identifiers.serviceId == nil, let phoneNumber = identifiers.phoneNumber else {
return
}
for serviceId in (cacheState.phoneNumberToServiceIds[phoneNumber] ?? []) {
if serviceId.isVisible {
identifiers.serviceId = serviceId.wrappedValue
return
}
// A "legacy" value can be resolved, but we know it's hidden (because the
// prior check didn't pass), so clear the phone number.
if isLegacyPhoneNumber {
identifiers.serviceId = serviceId.wrappedValue
identifiers.phoneNumber = nil
return
}
}
}
private func resolvePhoneNumber(
in identifiers: inout CachedAddress.Identifiers,
cacheState: CacheState
) {
guard identifiers.phoneNumber == nil, let serviceId = identifiers.serviceId else {
return
}
if let phoneNumber = cacheState.serviceIdToPhoneNumber[serviceId] {
if phoneNumber.isVisible {
identifiers.phoneNumber = phoneNumber.wrappedValue
return
}
// Unlike the prior method, we don't need special handling for
// isLegacyPhoneNumber in this case. The goal for "legacy" values is to
// resolve to an ACI, but if we reach this point, `identifier.serviceId` is
// an ACI (because PNIs don't set isVisible to false) and
// `identifier.phoneNumber` is nil.
}
}
/// Finds the best hash value for (serviceId, phoneNumber).
///
/// In general, we'll return an existing hash value if one exists, and we'll
/// generate a new random hash value if one doesn't exist. If we generate a
/// random hash value, we associate it with the provided identifier(s) for
/// future calls to this method.
///
/// Some edge cases worth documenting explicitly:
///
/// - If both identifiers are nonnil and have different hash values, the
/// hash value for `serviceId` will be returned. The hash value for
/// `phoneNumber` won't be updated for future calls to this method.
///
/// - If both identifiers are nonnil and only one of them has a hash value,
/// that value will be returned. The returned hash value will also be
/// associated with the other identifier for future calls to this method.
///
/// - If both identifiers are nil, this method is equivalent to
/// `Int.random(in: Int.min...Int.max)`. An address must have at least one
/// identifier to be considered valid, and addresses without identifiers
/// always return `false` from `isEqual:`, so it's perfectly acceptable for
/// each of these addresses to have its own hash value.
private func hashValue(cacheState: inout CacheState, serviceId: ServiceId?, phoneNumber: String?) -> Int {
let hashValue = (
serviceId.flatMap { cacheState.serviceIdHashValues[$0] }
?? phoneNumber.flatMap { cacheState.phoneNumberHashValues[$0] }
?? Int.random(in: Int.min...Int.max)
)
// We *never* change a hash value once it's been generated.
if let serviceId, cacheState.serviceIdHashValues[serviceId] == nil {
cacheState.serviceIdHashValues[serviceId] = hashValue
}
if let phoneNumber, cacheState.phoneNumberHashValues[phoneNumber] == nil {
cacheState.phoneNumberHashValues[phoneNumber] = hashValue
}
return hashValue
}
}
// MARK: - Unit Tests
#if TESTABLE_BUILD
extension SignalServiceAddress {
static func randomForTesting() -> SignalServiceAddress { SignalServiceAddress(Aci.randomForTesting()) }
static func isolatedRandomForTesting() -> SignalServiceAddress {
SignalServiceAddress(
serviceId: Aci.randomForTesting(),
phoneNumber: nil,
cache: SignalServiceAddressCache()
)
}
}
extension SignalServiceAddressCache {
func makeAddress(serviceId: ServiceId?, phoneNumber: E164?) -> SignalServiceAddress {
SignalServiceAddress(
serviceId: serviceId,
phoneNumber: phoneNumber?.stringValue,
cache: self
)
}
}
#endif