Signal-iOS/SignalServiceKit/src/Contacts/SignalRecipient.m
2021-03-25 09:24:27 -03:00

633 lines
25 KiB
Objective-C

//
// Copyright (c) 2021 Open Whisper Systems. All rights reserved.
//
#import "SignalRecipient.h"
#import "OWSDevice.h"
#import "ProfileManagerProtocol.h"
#import "SSKEnvironment.h"
#import "TSAccountManager.h"
#import "TSSocketManager.h"
#import <SignalServiceKit/SignalServiceKit-Swift.h>
NS_ASSUME_NONNULL_BEGIN
const NSUInteger SignalRecipientSchemaVersion = 1;
@interface SignalRecipient ()
@property (nonatomic) NSOrderedSet<NSNumber *> *devices;
@property (nonatomic) NSUInteger recipientSchemaVersion;
@end
#pragma mark -
@implementation SignalRecipient
- (instancetype)initWithUUIDString:(NSString *)uuidString
{
self = [super init];
if (!self) {
return self;
}
_recipientUUID = uuidString;
_recipientPhoneNumber = nil;
_recipientSchemaVersion = SignalRecipientSchemaVersion;
_devices = [NSOrderedSet orderedSetWithObject:@(OWSDevicePrimaryDeviceId)];
return self;
}
- (instancetype)initWithAddress:(SignalServiceAddress *)address
{
self = [super init];
if (!self) {
return self;
}
_recipientUUID = address.uuidString;
_recipientPhoneNumber = address.phoneNumber;
_recipientSchemaVersion = SignalRecipientSchemaVersion;
_devices = [NSOrderedSet orderedSetWithObject:@(OWSDevicePrimaryDeviceId)];
return self;
}
#if TESTABLE_BUILD
- (instancetype)initWithPhoneNumber:(nullable NSString *)phoneNumber
uuid:(nullable NSUUID *)uuid
devices:(NSArray<NSNumber *> *)devices
{
OWSAssertDebug(phoneNumber.length > 0 || uuid.UUIDString.length > 0);
self = [super init];
if (!self) {
return self;
}
_recipientUUID = uuid.UUIDString;
_recipientPhoneNumber = phoneNumber;
_recipientSchemaVersion = SignalRecipientSchemaVersion;
_devices = [NSOrderedSet orderedSetWithArray:devices];
return self;
}
#endif
- (nullable instancetype)initWithCoder:(NSCoder *)coder
{
self = [super initWithCoder:coder];
if (!self) {
return self;
}
if (_devices == nil) {
_devices = [NSOrderedSet new];
}
// Migrating from an everyone has a phone number world to a
// world in which we have UUIDs
if (_recipientSchemaVersion < 1) {
// Copy uniqueId to recipientPhoneNumber
_recipientPhoneNumber = [coder decodeObjectForKey:@"uniqueId"];
OWSAssert(_recipientPhoneNumber != nil);
}
// Since we use device count to determine whether a user is registered or not,
// ensure the local user always has at least *this* device.
if (![_devices containsObject:@(OWSDevicePrimaryDeviceId)]) {
if (self.address.isLocalAddress) {
DDLogInfo(@"Adding primary device to self recipient.");
[self addDevices:[NSSet setWithObject:@(OWSDevicePrimaryDeviceId)]];
}
}
_recipientSchemaVersion = SignalRecipientSchemaVersion;
return self;
}
// --- CODE GENERATION MARKER
// This snippet is generated by /Scripts/sds_codegen/sds_generate.py. Do not manually edit it, instead run `sds_codegen.sh`.
// clang-format off
- (instancetype)initWithGrdbId:(int64_t)grdbId
uniqueId:(NSString *)uniqueId
devices:(NSOrderedSet<NSNumber *> *)devices
recipientPhoneNumber:(nullable NSString *)recipientPhoneNumber
recipientUUID:(nullable NSString *)recipientUUID
{
self = [super initWithGrdbId:grdbId
uniqueId:uniqueId];
if (!self) {
return self;
}
_devices = devices;
_recipientPhoneNumber = recipientPhoneNumber;
_recipientUUID = recipientUUID;
return self;
}
// clang-format on
// --- CODE GENERATION MARKER
+ (AnySignalRecipientFinder *)recipientFinder
{
return [AnySignalRecipientFinder new];
}
+ (nullable instancetype)getRecipientForAddress:(SignalServiceAddress *)address
mustHaveDevices:(BOOL)mustHaveDevices
transaction:(SDSAnyReadTransaction *)transaction
{
OWSAssertDebug(transaction);
OWSAssertDebug(address.isValid);
SignalRecipient *_Nullable signalRecipient =
[self.modelReadCaches.signalRecipientReadCache getSignalRecipientForAddress:address transaction:transaction];
if (mustHaveDevices && signalRecipient.devices.count < 1) {
return nil;
}
return signalRecipient;
}
#pragma mark -
- (void)addDevices:(NSSet<NSNumber *> *)devices
{
OWSAssertDebug(devices.count > 0);
NSMutableOrderedSet<NSNumber *> *updatedDevices = [self.devices mutableCopy];
[updatedDevices unionSet:devices];
self.devices = [updatedDevices copy];
}
- (void)removeDevices:(NSSet<NSNumber *> *)devices
{
OWSAssertDebug(devices.count > 0);
NSMutableOrderedSet<NSNumber *> *updatedDevices = [self.devices mutableCopy];
[updatedDevices minusSet:devices];
self.devices = [updatedDevices copy];
}
+ (void)updateWithAddress:(SignalServiceAddress *)address
devicesToAdd:(nullable NSArray<NSNumber *> *)devicesToAdd
devicesToRemove:(nullable NSArray<NSNumber *> *)devicesToRemove
transaction:(SDSAnyWriteTransaction *)transaction
{
OWSAssertDebug(transaction);
OWSAssertDebug(devicesToAdd.count > 0 || devicesToRemove.count > 0);
SignalRecipient *recipient = [self getOrCreateLowTrustRecipientWithAdddress:address transaction:transaction];
[recipient updateWithDevicesToAdd:devicesToAdd devicesToRemove:devicesToRemove transaction:transaction];
}
- (void)updateWithDevicesToAdd:(nullable NSArray<NSNumber *> *)devicesToAdd
devicesToRemove:(nullable NSArray<NSNumber *> *)devicesToRemove
transaction:(SDSAnyWriteTransaction *)transaction
{
OWSAssertDebug(transaction);
OWSAssertDebug(devicesToAdd.count > 0 || devicesToRemove.count > 0);
// Add before we remove, since removeDevicesFromRecipient:...
// can markRecipientAsUnregistered:... if the recipient has
// no devices left.
if (devicesToAdd.count > 0) {
OWSLogInfo(@"devicesToAdd: %@ for %@", devicesToAdd, self.address);
[self updateWithDevicesToAdd:[NSSet setWithArray:devicesToAdd] transaction:transaction];
}
if (devicesToRemove.count > 0) {
OWSLogInfo(@"devicesToRemove: %@ for %@", devicesToRemove, self.address);
[self updateWithDevicesToRemove:[NSSet setWithArray:devicesToRemove] transaction:transaction];
}
// Device changes
dispatch_async(dispatch_get_main_queue(), ^{
// Device changes can affect the UD access mode for a recipient,
// so we need to fetch the profile for this user to update UD access mode.
[self.profileManager fetchProfileForAddress:self.address];
if (self.address.isLocalAddress) {
[self.socketManager cycleSocket];
}
});
}
- (void)updateWithDevicesToAdd:(NSSet<NSNumber *> *)devices transaction:(SDSAnyWriteTransaction *)transaction
{
OWSAssertDebug(transaction);
OWSAssertDebug(devices.count > 0);
OWSLogDebug(@"adding devices: %@, to recipient: %@", devices, self);
[self anyReloadWithTransaction:transaction];
[self anyUpdateWithTransaction:transaction
block:^(SignalRecipient *signalRecipient) {
[signalRecipient addDevices:devices];
}];
}
- (void)updateWithDevicesToRemove:(NSSet<NSNumber *> *)devices transaction:(SDSAnyWriteTransaction *)transaction
{
OWSAssertDebug(transaction);
OWSAssertDebug(devices.count > 0);
OWSLogDebug(@"removing devices: %@, from registered recipient: %@", devices, self);
[self anyReloadWithTransaction:transaction ignoreMissing:YES];
[self anyUpdateWithTransaction:transaction
block:^(SignalRecipient *signalRecipient) {
[signalRecipient removeDevices:devices];
}];
}
#pragma mark -
- (SignalServiceAddress *)address
{
return [[SignalServiceAddress alloc] initWithUuidString:self.recipientUUID phoneNumber:self.recipientPhoneNumber];
}
#pragma mark -
- (NSComparisonResult)compare:(SignalRecipient *)other
{
return [self.address compare:other.address];
}
- (void)anyWillInsertWithTransaction:(SDSAnyWriteTransaction *)transaction
{
[super anyWillInsertWithTransaction:transaction];
OWSLogVerbose(@"Inserted signal recipient: %@ (%lu)", self.address, (unsigned long)self.devices.count);
}
- (void)anyWillUpdateWithTransaction:(SDSAnyWriteTransaction *)transaction
{
[super anyWillUpdateWithTransaction:transaction];
OWSLogVerbose(@"Updated signal recipient: %@ (%lu)", self.address, (unsigned long)self.devices.count);
}
+ (BOOL)isRegisteredRecipient:(SignalServiceAddress *)address transaction:(SDSAnyReadTransaction *)transaction
{
OWSAssertDebug(transaction);
OWSAssertDebug(address.isValid);
return nil != [self getRecipientForAddress:address mustHaveDevices:YES transaction:transaction];
}
+ (SignalRecipient *)markRecipientAsRegisteredAndGet:(SignalServiceAddress *)address
trustLevel:(SignalRecipientTrustLevel)trustLevel
transaction:(SDSAnyWriteTransaction *)transaction
{
OWSAssertDebug(address.isValid);
OWSAssertDebug(transaction);
switch (trustLevel) {
case SignalRecipientTrustLevelLow:
return [self getOrCreateLowTrustRecipientWithAdddress:address transaction:transaction];
case SignalRecipientTrustLevelHigh:
return [self getOrCreateHighTrustRecipientWithAddress:address markAsRegistered:YES transaction:transaction];
}
}
+ (SignalRecipient *)getOrCreateLowTrustRecipientWithAdddress:(SignalServiceAddress *)address
transaction:(SDSAnyWriteTransaction *)transaction
{
OWSAssertDebug(address.isValid);
OWSAssertDebug(transaction);
SignalRecipient *_Nullable phoneNumberInstance = nil;
SignalRecipient *_Nullable uuidInstance = nil;
if (address.phoneNumber != nil) {
phoneNumberInstance = [self.recipientFinder signalRecipientForPhoneNumber:address.phoneNumber
transaction:transaction];
}
if (address.uuid != nil) {
uuidInstance = [self.recipientFinder signalRecipientForUUID:address.uuid transaction:transaction];
}
// Low trust updates should never update the database, unless
// there is no matching record for the UUID, in which case we
// can create a new UUID only record (we don't want to associate
// it with the phone number) or there is no UUID, in which case
// we will record the phone number alone.
if (uuidInstance) {
return uuidInstance;
} else if (address.uuidString) {
OWSLogDebug(@"creating new low trust recipient with UUID: %@", address.uuidString);
SignalRecipient *newInstance = [[self alloc] initWithUUIDString:address.uuidString];
[newInstance anyInsertWithTransaction:transaction];
// Record with the new contact in the social graph
[self.storageServiceManager recordPendingUpdatesWithUpdatedAccountIds:@[ newInstance.accountId ]];
return newInstance;
} else if (phoneNumberInstance) {
return phoneNumberInstance;
} else {
OWSAssertDebug(address.phoneNumber);
OWSLogDebug(@"creating new low trust recipient with phoneNumber: %@", address.phoneNumber);
SignalRecipient *newInstance = [[self alloc] initWithAddress:address];
[newInstance anyInsertWithTransaction:transaction];
// Record with the new contact in the social graph
[self.storageServiceManager recordPendingUpdatesWithUpdatedAccountIds:@[ newInstance.accountId ]];
return newInstance;
}
}
+ (SignalRecipient *)getOrCreateHighTrustRecipientWithAddress:(SignalServiceAddress *)address
markAsRegistered:(BOOL)markAsRegistered
transaction:(SDSAnyWriteTransaction *)transaction
{
OWSAssertDebug(address.isValid);
OWSAssertDebug(transaction);
SignalRecipient *_Nullable phoneNumberInstance = nil;
SignalRecipient *_Nullable uuidInstance = nil;
if (address.phoneNumber != nil) {
phoneNumberInstance = [self.recipientFinder signalRecipientForPhoneNumber:address.phoneNumber
transaction:transaction];
}
if (address.uuid != nil) {
uuidInstance = [self.recipientFinder signalRecipientForUUID:address.uuid transaction:transaction];
}
// High trust updates will fully update the database to reflect
// the new mapping in a given address, if any changes are present.
//
// In general, the rules we follow when applying changes are:
// * UUIDs are immutable and representative of an account. If the UUID
// has changed we must treat it as an entirely new contact.
// * Phone numbers are transient and can move freely between UUIDs. When
// they do, we must backfill the database to reflect the change.
BOOL shouldUpdate = NO;
SignalRecipient *_Nullable existingInstance = nil;
if (uuidInstance && phoneNumberInstance) {
// These are the same and both fully complete, we have no extra work to do.
if ([uuidInstance.uniqueId isEqualToString:phoneNumberInstance.uniqueId]) {
existingInstance = phoneNumberInstance;
// These are the same, but not fully complete. We need to merge them.
} else if (phoneNumberInstance.recipientUUID == nil && uuidInstance.recipientPhoneNumber == nil) {
existingInstance = [self mergeUUIDInstance:uuidInstance
andPhoneNumberInstance:phoneNumberInstance
transaction:transaction];
shouldUpdate = YES;
// Update the SignalServiceAddressCache mappings with the now fully-qualified recipient.
[SSKEnvironment.shared.signalServiceAddressCache updateMappingWithUuid:address.uuid
phoneNumber:address.phoneNumber];
// The UUID differs between the two records, we need to migrate the phone
// number to the UUID instance.
} else {
OWSLogWarn(@"Learned phoneNumber (%@) now belongs to uuid (%@).", address.phoneNumber, address.uuid);
// Ordering is critical here. We must remove the phone number
// from the old recipient *before* we assign the phone number
// to the new recipient, in case there are any legacy phone
// number only records in the database.
shouldUpdate = YES;
OWSAssertDebug(phoneNumberInstance.recipientUUID != nil);
[phoneNumberInstance changePhoneNumber:nil transaction:transaction.unwrapGrdbWrite];
[phoneNumberInstance anyOverwritingUpdateWithTransaction:transaction];
[uuidInstance changePhoneNumber:address.phoneNumber transaction:transaction.unwrapGrdbWrite];
existingInstance = uuidInstance;
}
} else if (phoneNumberInstance) {
if (address.uuidString && phoneNumberInstance.recipientUUID != nil) {
OWSLogWarn(@"Learned phoneNumber (%@) now belongs to uuid (%@).", address.phoneNumber, address.uuid);
// The UUID associated with this phone number has changed, we must
// clear the phone number from this instance and create a new instance.
[phoneNumberInstance changePhoneNumber:nil transaction:transaction.unwrapGrdbWrite];
[phoneNumberInstance anyOverwritingUpdateWithTransaction:transaction];
} else {
if (address.uuidString) {
OWSLogWarn(
@"Learned uuid (%@) is associated with phoneNumber (%@).", address.uuidString, address.phoneNumber);
shouldUpdate = YES;
phoneNumberInstance.recipientUUID = address.uuidString;
// Update the SignalServiceAddressCache mappings with the now fully-qualified recipient.
[SSKEnvironment.shared.signalServiceAddressCache updateMappingWithUuid:address.uuid
phoneNumber:address.phoneNumber];
}
existingInstance = phoneNumberInstance;
}
} else if (uuidInstance) {
if (address.phoneNumber) {
if (uuidInstance.recipientPhoneNumber == nil) {
OWSLogWarn(
@"Learned uuid (%@) is associated with phoneNumber (%@).", address.uuidString, address.phoneNumber);
} else {
OWSLogWarn(@"Learned uuid (%@) changed from old phoneNumber (%@) to new phoneNumber (%@)",
address.uuidString,
existingInstance.recipientPhoneNumber,
address.phoneNumber);
}
shouldUpdate = YES;
[uuidInstance changePhoneNumber:address.phoneNumber transaction:transaction.unwrapGrdbWrite];
}
existingInstance = uuidInstance;
}
if (existingInstance == nil) {
OWSLogDebug(@"creating new high trust recipient with address: %@", address);
SignalRecipient *newInstance = [[self alloc] initWithAddress:address];
[newInstance anyInsertWithTransaction:transaction];
// Update the SignalServiceAddressCache mappings with the new recipient.
if (address.uuid) {
[SSKEnvironment.shared.signalServiceAddressCache updateMappingWithUuid:address.uuid
phoneNumber:address.phoneNumber];
}
// Record with the new contact in the social graph
[self.storageServiceManager recordPendingUpdatesWithUpdatedAccountIds:@[ newInstance.accountId ]];
return newInstance;
}
if (markAsRegistered && existingInstance.devices.count == 0) {
shouldUpdate = YES;
// We know they're registered, so make sure they have at least one device.
// We assume it's the default device. If we're wrong, the service will correct us when we
// try to send a message to them
existingInstance.devices = [NSOrderedSet orderedSetWithObject:@(OWSDevicePrimaryDeviceId)];
}
// Record the updated contact in the social graph
if (shouldUpdate) {
OWSAssertDebug([existingInstance.devices containsObject:@(OWSDevicePrimaryDeviceId)]);
[existingInstance anyOverwritingUpdateWithTransaction:transaction];
[self.storageServiceManager recordPendingUpdatesWithUpdatedAccountIds:@[ existingInstance.accountId ]];
}
return existingInstance;
}
+ (SignalRecipient *)mergeUUIDInstance:(SignalRecipient *)uuidInstance
andPhoneNumberInstance:(SignalRecipient *)phoneNumberInstance
transaction:(SDSAnyWriteTransaction *)transaction
{
OWSAssertDebug(uuidInstance.recipientPhoneNumber == nil ||
[NSObject isNullableObject:uuidInstance.recipientPhoneNumber equalTo:phoneNumberInstance.recipientPhoneNumber]);
OWSAssertDebug(phoneNumberInstance.recipientUUID == nil ||
[NSObject isNullableObject:phoneNumberInstance.recipientUUID equalTo:uuidInstance.recipientUUID]);
// We have separate recipients in the db for the uuid and phone number.
// There isn't an ideal way to do this, but we need to converge on one
// recipient and discard the other.
//
// TODO: Should we clean up any state related to the discarded recipient?
SignalRecipient *_Nullable winningInstance = nil;
// We try to preserve the recipient that has a session.
BOOL hasSessionForUuid = [self.sessionStore containsActiveSessionForAccountId:uuidInstance.accountId
deviceId:OWSDevicePrimaryDeviceId
transaction:transaction];
BOOL hasSessionForPhoneNumber = [self.sessionStore containsActiveSessionForAccountId:phoneNumberInstance.accountId
deviceId:OWSDevicePrimaryDeviceId
transaction:transaction];
if (SSKDebugFlags.verboseSignalRecipientLogging) {
OWSLogInfo(@"phoneNumberInstance: %@", phoneNumberInstance);
OWSLogInfo(@"uuidInstance: %@", uuidInstance);
OWSLogInfo(@"hasSessionForUuid: %@", @(hasSessionForUuid));
OWSLogInfo(@"hasSessionForPhoneNumber: %@", @(hasSessionForPhoneNumber));
}
// We want to retain the phone number recipient only if it has a session and the UUID recipient doesn't.
// Historically, we tried to be clever and pick the session that had seen more use,
// but merging sessions should only happen in exceptional circumstances these days.
if (hasSessionForUuid) {
OWSLogWarn(@"Discarding phone number recipient in favor of uuid recipient.");
winningInstance = uuidInstance;
[phoneNumberInstance anyRemoveWithTransaction:transaction];
} else {
OWSLogWarn(@"Discarding uuid recipient in favor of phone number recipient.");
winningInstance = phoneNumberInstance;
[uuidInstance anyRemoveWithTransaction:transaction];
}
// Make sure the winning instance is fully qualified.
winningInstance.recipientPhoneNumber = phoneNumberInstance.recipientPhoneNumber;
winningInstance.recipientUUID = uuidInstance.recipientUUID;
[OWSUserProfile mergeUserProfilesIfNecessaryForAddress:winningInstance.address transaction:transaction];
return winningInstance;
}
+ (SignalRecipient *)markRecipientAsRegisteredAndGet:(SignalServiceAddress *)address
deviceId:(UInt32)deviceId
trustLevel:(SignalRecipientTrustLevel)trustLevel
transaction:(SDSAnyWriteTransaction *)transaction
{
OWSAssertDebug(address.isValid);
OWSAssertDebug(deviceId > 0);
OWSAssertDebug(transaction);
SignalRecipient *recipient = [self markRecipientAsRegisteredAndGet:address
trustLevel:trustLevel
transaction:transaction];
if (![recipient.devices containsObject:@(deviceId)]) {
OWSLogDebug(@"Adding device %u to existing recipient.", (unsigned int)deviceId);
[recipient anyReloadWithTransaction:transaction];
[recipient anyUpdateWithTransaction:transaction
block:^(SignalRecipient *signalRecipient) {
[signalRecipient addDevices:[NSSet setWithObject:@(deviceId)]];
}];
}
return recipient;
}
+ (void)markRecipientAsUnregistered:(SignalServiceAddress *)address transaction:(SDSAnyWriteTransaction *)transaction
{
OWSAssertDebug(address.isValid);
OWSAssertDebug(transaction);
SignalRecipient *recipient = [self getOrCreateLowTrustRecipientWithAdddress:address transaction:transaction];
if (recipient.devices.count > 0) {
OWSLogDebug(@"Marking recipient as not registered: %@", address);
[recipient anyUpdateWithTransaction:transaction
block:^(SignalRecipient *signalRecipient) {
signalRecipient.devices = [NSOrderedSet new];
}];
// Remove the contact from our social graph
[self.storageServiceManager recordPendingDeletionsWithDeletedAccountIds:@[ recipient.accountId ]];
}
}
- (void)anyDidInsertWithTransaction:(SDSAnyWriteTransaction *)transaction
{
[super anyDidInsertWithTransaction:transaction];
[self.modelReadCaches.signalRecipientReadCache didInsertOrUpdateSignalRecipient:self transaction:transaction];
}
- (void)anyDidUpdateWithTransaction:(SDSAnyWriteTransaction *)transaction
{
[super anyDidUpdateWithTransaction:transaction];
[self.modelReadCaches.signalRecipientReadCache didInsertOrUpdateSignalRecipient:self transaction:transaction];
}
- (void)anyDidRemoveWithTransaction:(SDSAnyWriteTransaction *)transaction
{
[super anyDidRemoveWithTransaction:transaction];
[self.modelReadCaches.signalRecipientReadCache didRemoveSignalRecipient:self transaction:transaction];
[self.storageServiceManager recordPendingDeletionsWithDeletedAccountIds:@[ self.accountId ]];
}
+ (BOOL)shouldBeIndexedForFTS
{
return YES;
}
- (void)removePhoneNumberForDatabaseMigration
{
OWSAssertDebug(self.recipientUUID != nil);
OWSAssertDebug(self.recipientPhoneNumber != nil);
_recipientPhoneNumber = nil;
}
@end
NS_ASSUME_NONNULL_END