Signal-iOS/SignalServiceKit/src/Contacts/SignalServiceAddress.swift
2023-05-24 15:50:17 -07:00

630 lines
24 KiB
Swift

//
// Copyright 2019 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import Foundation
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($0) } }
@objc
public var uuid: UUID? { serviceId?.uuidValue }
@objc
public var uuidString: String? { uuid?.uuidString }
// MARK: - Initializers
@objc
public convenience init(e164ObjC: E164ObjC) {
self.init(e164ObjC.wrappedValue)
}
public convenience init(_ e164: E164) {
self.init(phoneNumber: e164.stringValue)
}
@objc
public convenience init(phoneNumber: String) {
self.init(uuidString: nil, phoneNumber: phoneNumber)
}
@objc
public convenience init(uuidString: String) {
self.init(uuidString: uuidString, phoneNumber: nil)
}
@objc
public convenience init(serviceIdObjC: ServiceIdObjC) {
self.init(serviceIdObjC.wrappedValue)
}
public convenience init(_ serviceId: ServiceId) {
self.init(uuid: serviceId.uuidValue)
}
@objc
public convenience init(uuid: UUID) {
self.init(uuid: uuid, phoneNumber: nil)
}
@objc
public convenience init(uuidString: String?, phoneNumber: String?) {
let uuid: UUID?
if let uuidString {
uuid = UUID(uuidString: uuidString)
owsAssertDebug(uuid != nil, "Unexpectedly initialized signal service address with invalid uuid")
} else {
uuid = nil
}
self.init(uuid: uuid, phoneNumber: phoneNumber)
}
@objc
public convenience init(uuid: UUID?, phoneNumber: String?) {
self.init(uuid: uuid, phoneNumber: phoneNumber, ignoreCache: false)
}
public convenience init(uuid: UUID?, e164: E164?) {
self.init(uuid: uuid, phoneNumber: e164?.stringValue)
}
internal convenience init(from address: ProtocolAddress) {
if let uuid = UUID(uuidString: address.name) {
self.init(uuid: uuid)
} else {
// FIXME: What happens if this is *not* a valid phone number?
self.init(phoneNumber: address.name)
}
}
private convenience init(decodedUuid: UUID?, decodedPhoneNumber: String?) {
self.init(
uuid: decodedUuid,
phoneNumber: decodedPhoneNumber,
cache: Self.signalServiceAddressCache,
// If we know the UUID, let the cache fill in the phone number when
// possible. (This avoids decoding stale mappings that may exist.)
cachePolicy: .preferCachedPhoneNumberAndListenForUpdates
)
}
@objc
public convenience init(uuid: UUID?, phoneNumber: String?, ignoreCache: Bool) {
self.init(
uuid: uuid,
phoneNumber: phoneNumber,
cache: Self.signalServiceAddressCache,
cachePolicy: ignoreCache ? .ignoreCache : .preferInitialPhoneNumberAndListenForUpdates
)
}
public init(
uuid: UUID?,
phoneNumber: String?,
cache: SignalServiceAddressCache,
cachePolicy: SignalServiceAddressCache.CachePolicy
) {
if let phoneNumber, phoneNumber.isEmpty {
owsFailDebug("Unexpectedly initialized signal service address with invalid phone number")
}
self.cachedAddress = cache.registerAddress(
proposedIdentifiers: CachedAddress.Identifiers(
serviceId: uuid.map { ServiceId($0) },
phoneNumber: phoneNumber
),
cachePolicy: cachePolicy
)
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?.uuidValue, 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 decodedUuid: UUID? = try container.decodeIfPresent(UUID.self, forKey: .backingUuid)
let decodedPhoneNumber = try container.decodeIfPresent(String.self, forKey: .backingPhoneNumber)
self.init(decodedUuid: decodedUuid, decodedPhoneNumber: decodedPhoneNumber)
}
// MARK: - NSSecureCoding
public static let supportsSecureCoding: Bool = true
public func encode(with aCoder: NSCoder) {
let identifiers = cachedAddress.identifiers.get()
aCoder.encode(identifiers.serviceId?.uuidValue, 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 decodedUuid = aDecoder.decodeObject(of: NSUUID.self, forKey: "backingUuid") as UUID?
let decodedPhoneNumber = aDecoder.decodeObject(of: NSString.self, forKey: "backingPhoneNumber") as String?
self.init(decodedUuid: decodedUuid, decodedPhoneNumber: decodedPhoneNumber)
}
// 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 TSAccountManager.localAddress == 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.uuidValue.uuidString
}
owsFailDebug("unexpectedly have no backing value")
return ""
}
@objc
public var serviceIdentifier: String? {
let identifiers = cachedAddress.identifiers.get()
if let serviceId = identifiers.serviceId {
return serviceId.uuidValue.uuidString
}
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?.uuidValue.uuidString,
phoneNumber: identifiers.phoneNumber
)
}
public static func addressComponentsDescription(uuidString: String?, phoneNumber: String?) -> String {
var splits = [String]()
if let uuid = uuidString?.nilIfEmpty {
splits.append("serviceId: " + uuid)
}
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: AtomicLock())
}
}
@objc
public class SignalServiceAddressCache: NSObject {
private let state = AtomicValue(CacheState(), lock: AtomicLock())
private struct CacheState {
var serviceIdHashValues = [ServiceId: Int]()
var phoneNumberHashValues = [String: Int]()
var serviceIdToPhoneNumber = [ServiceId: String]()
var phoneNumberToServiceIds = [String: [ServiceId]]()
var serviceIdCachedAddresses = [ServiceId: [CachedAddress]]()
var phoneNumberOnlyCachedAddresses = [String: [CachedAddress]]()
}
@objc
func warmCaches() {
owsAssertDebug(GRDBSchemaMigrator.areMigrationsComplete)
databaseStorage.read { transaction in
if let localAddress = tsAccountManager.localAddress(with: transaction) {
updateRecipient(
serviceId: localAddress.serviceId,
// PNI TODO: Fetch our own PNI once it's stored on our SignalRecipient.
//
// (Even though our own PNI may be available at this point, we should have
// a recipient for ourselves, so we'd immediately overwrite it during the
// `anyEnumerate` below.)
pniString: nil,
phoneNumber: localAddress.phoneNumber
)
}
SignalRecipient.anyEnumerate(transaction: transaction) { recipient, _ in
self.updateRecipient(recipient)
}
}
}
func updateRecipient(_ signalRecipient: SignalRecipient) {
updateRecipient(
serviceId: signalRecipient.serviceId,
// PNI TODO: Fetch the recipientPNI once that property is available.
pniString: nil,
phoneNumber: signalRecipient.phoneNumber
)
}
private func updateRecipient(serviceId: ServiceId?, pniString: String?, phoneNumber: String?) {
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 oldServiceIds: [ServiceId] = cacheState.phoneNumberToServiceIds[phoneNumber] ?? []
let newServiceIds: [ServiceId] = [
serviceId,
ServiceId(uuidString: pniString)
].compacted()
// If this phone number still points at the same ServiceIds, there's
// nothing to change.
guard newServiceIds != oldServiceIds else {
return
}
cacheState.phoneNumberToServiceIds[phoneNumber] = newServiceIds
// These ServiceIds are no longer associated with `phoneNumber`.
for serviceId in Set(oldServiceIds).subtracting(newServiceIds) {
cacheState.serviceIdToPhoneNumber[serviceId] = nil
cacheState.serviceIdCachedAddresses[serviceId]?.forEach { cachedAddress in
cachedAddress.identifiers.update { $0.phoneNumber = nil }
}
}
// These ServiceIds are now associated with `phoneNumber`.
for serviceId in Set(newServiceIds).subtracting(oldServiceIds) {
let oldPhoneNumber = cacheState.serviceIdToPhoneNumber.updateValue(phoneNumber, forKey: serviceId)
cacheState.serviceIdCachedAddresses[serviceId]?.forEach { cachedAddress in
cachedAddress.identifiers.update { $0.phoneNumber = phoneNumber }
}
// If this ServiceId was associated with some other phone number, we need
// to break that association.
if let oldPhoneNumber {
owsAssertDebug(oldPhoneNumber != phoneNumber)
cacheState.phoneNumberToServiceIds[oldPhoneNumber]?.removeAll(where: { $0 == serviceId })
}
// 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: serviceId, phoneNumber: phoneNumber)
}
// 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, though,
// 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 nonnil UUID for a SignalServiceAddress remains stable.
if let preferredServiceId = newServiceIds.first {
cacheState.phoneNumberOnlyCachedAddresses.removeValue(forKey: phoneNumber)?.forEach { cachedAddress in
cachedAddress.identifiers.update { $0.serviceId = preferredServiceId }
// This address has a serviceId now -- track that serviceId for future updates.
cacheState.serviceIdCachedAddresses[preferredServiceId, default: []].append(cachedAddress)
}
}
}
}
public enum CachePolicy {
/// Prefers a nonnil phone number from the initializer. This is useful in
/// cases where the initializer has more recent mappings than what's
/// available in the cache. If the phone number changes in the future, the
/// address will be dynamically updated.
case preferInitialPhoneNumberAndListenForUpdates
/// Prefers a nonnil phone number from the cache. This handles cases where
/// "stale" data may be provided in the initializer. If the phone number
/// changes in the future, the address will be dynamically updated.
case preferCachedPhoneNumberAndListenForUpdates
/// Never retrieves either value from the cache. Never updated when a new
/// mapping is learned. The hash value *is* retrieved from the "cache".
/// These addresses shouldn't be put in a Set or used as Dictionary keys.
case ignoreCache
}
fileprivate func registerAddress(proposedIdentifiers: CachedAddress.Identifiers, cachePolicy: CachePolicy) -> CachedAddress {
state.update { cacheState in
let resolvedIdentifiers: CachedAddress.Identifiers
switch cachePolicy {
case .ignoreCache:
resolvedIdentifiers = proposedIdentifiers
case .preferInitialPhoneNumberAndListenForUpdates:
resolvedIdentifiers = resolveIdentifiers(proposedIdentifiers, preferInitialPhoneNumber: true, cacheState: cacheState)
case .preferCachedPhoneNumberAndListenForUpdates:
resolvedIdentifiers = resolveIdentifiers(proposedIdentifiers, preferInitialPhoneNumber: false, 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
}
switch cachePolicy {
case .preferInitialPhoneNumberAndListenForUpdates, .preferCachedPhoneNumberAndListenForUpdates:
if let serviceId = resolvedIdentifiers.serviceId {
return getOrCreateCachedAddress(key: serviceId, in: &cacheState.serviceIdCachedAddresses)
}
if let phoneNumber = resolvedIdentifiers.phoneNumber {
return getOrCreateCachedAddress(key: phoneNumber, in: &cacheState.phoneNumberOnlyCachedAddresses)
}
fallthrough
case .ignoreCache:
return CachedAddress(hashValue: hashValue, identifiers: resolvedIdentifiers)
}
}
}
/// Populates missing/stale identifiers populated from the cache.
///
/// - Parameter preferInitialPhoneNumber: If true, the phone number value
/// from `proposedIdentifiers` will be used if it's nonnil; if it's nil, the
/// value from the cache will be used. If false, the phone number value from
/// the cache will be used if it's nonnil; if it's nil, the value from
/// `proposedIdentifiers` will be used.
private func resolveIdentifiers(
_ proposedIdentifiers: CachedAddress.Identifiers,
preferInitialPhoneNumber: Bool,
cacheState: CacheState
) -> CachedAddress.Identifiers {
CachedAddress.Identifiers(
serviceId: (
// We *always* prefer the provided serviceId.
proposedIdentifiers.serviceId
?? proposedIdentifiers.phoneNumber.flatMap { cacheState.phoneNumberToServiceIds[$0]?.first }
),
phoneNumber: preferInitialPhoneNumber ? (
proposedIdentifiers.phoneNumber
?? proposedIdentifiers.serviceId.flatMap { cacheState.serviceIdToPhoneNumber[$0] }
) : (
proposedIdentifiers.serviceId.flatMap { cacheState.serviceIdToPhoneNumber[$0] }
?? proposedIdentifiers.phoneNumber
)
)
}
/// 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
}
}
public extension UUID {
func asSignalServiceAddress() -> SignalServiceAddress {
return SignalServiceAddress(uuid: self)
}
}
// MARK: - Unit Tests
#if TESTABLE_BUILD
extension SignalServiceAddressCache {
func makeAddress(serviceId: ServiceId?, phoneNumber: E164?) -> SignalServiceAddress {
SignalServiceAddress(
uuid: serviceId?.uuidValue,
phoneNumber: phoneNumber?.stringValue,
cache: self,
cachePolicy: .preferInitialPhoneNumberAndListenForUpdates
)
}
}
#endif