Signal-iOS/SignalMessaging/profiles/OWSProfileManager.m
George Nachman 36119e4367 Introduce Refinery to help batch DB queries.
This PR is the first in a series that will
optimize looking up full names of group members.

The biggest source of slowness when opening a
group chat is looking up the full names of group
members in the search for duplicates. It is slow
because it requires multiple db queries for each
member.

The characterstic feature of this algorithm is the
iterative process of assigning names to signal
addresses. For example, some contacts' names may
be cached. For others, their profiles must be
fetched. For those without profiles, their phone
numbers must be formatted (which requires fetching
SignalAccounts). For those without phone numbers,
their user names must be formatted.

This PR creates a class called Refinery. Its job
is to make it easy to assign values to keys
through multiple passes, where each pass may
succeed only for a subset of keys.

This is useful because we will eventually issue a
single DB query for some of these passes.
2022-03-04 10:44:07 -08:00

2003 lines
78 KiB
Objective-C

//
// Copyright (c) 2022 Open Whisper Systems. All rights reserved.
//
#import "OWSProfileManager.h"
#import "Environment.h"
#import <SignalCoreKit/Cryptography.h>
#import <SignalCoreKit/NSData+OWS.h>
#import <SignalCoreKit/NSDate+OWS.h>
#import <SignalCoreKit/NSString+OWS.h>
#import <SignalCoreKit/SignalCoreKit-Swift.h>
#import <SignalMessaging/SignalMessaging-Swift.h>
#import <SignalServiceKit/AppContext.h>
#import <SignalServiceKit/AppReadiness.h>
#import <SignalServiceKit/HTTPUtils.h>
#import <SignalServiceKit/MIMETypeUtil.h>
#import <SignalServiceKit/MessageSender.h>
#import <SignalServiceKit/NSData+Image.h>
#import <SignalServiceKit/OWSFileSystem.h>
#import <SignalServiceKit/OWSProfileKeyMessage.h>
#import <SignalServiceKit/OWSSignalService.h>
#import <SignalServiceKit/OWSUpload.h>
#import <SignalServiceKit/OWSUserProfile.h>
#import <SignalServiceKit/SSKEnvironment.h>
#import <SignalServiceKit/SignalServiceKit-Swift.h>
#import <SignalServiceKit/TSAccountManager.h>
#import <SignalServiceKit/TSGroupThread.h>
#import <SignalServiceKit/TSThread.h>
#import <SignalServiceKit/UIImage+OWS.h>
NS_ASSUME_NONNULL_BEGIN
const NSUInteger kOWSProfileManager_MaxAvatarDiameterPixels = 1024;
const NSString *kNSNotificationKey_UserProfileWriter = @"kNSNotificationKey_UserProfileWriter";
@interface OWSProfileManager ()
// This property can be accessed on any thread, while synchronized on self.
@property (atomic, readonly) OWSUserProfile *localUserProfile;
@property (nonatomic, readonly) AtomicUInt *profileAvatarDataLoadCounter;
@property (nonatomic, readonly) AtomicUInt *profileAvatarImageLoadCounter;
@end
#pragma mark -
@implementation OWSProfileSnapshot
- (instancetype)initWithGivenName:(nullable NSString *)givenName
familyName:(nullable NSString *)familyName
fullName:(nullable NSString *)fullName
bio:(nullable NSString *)bio
bioEmoji:(nullable NSString *)bioEmoji
username:(nullable NSString *)username
avatarData:(nullable NSData *)avatarData
profileBadgeInfo:(nullable NSArray<OWSUserProfileBadgeInfo *> *)badgeArray
{
self = [super init];
if (!self) {
return self;
}
_givenName = givenName;
_familyName = familyName;
_fullName = fullName;
_bio = bio;
_bioEmoji = bioEmoji;
_username = username;
_avatarData = avatarData;
_profileBadgeInfo = [badgeArray copy];
return self;
}
@end
#pragma mark -
// Access to most state should happen while synchronized on the profile manager.
// Writes should happen off the main thread, wherever possible.
@implementation OWSProfileManager
@synthesize localUserProfile = _localUserProfile;
- (instancetype)initWithDatabaseStorage:(SDSDatabaseStorage *)databaseStorage
{
self = [super init];
if (!self) {
return self;
}
OWSAssertIsOnMainThread();
OWSAssertDebug(databaseStorage);
_profileAvatarDataLoadCounter = [[AtomicUInt alloc] init:0];
_profileAvatarImageLoadCounter = [[AtomicUInt alloc] init:0];
_whitelistedPhoneNumbersStore =
[[SDSKeyValueStore alloc] initWithCollection:@"kOWSProfileManager_UserWhitelistCollection"];
_whitelistedUUIDsStore =
[[SDSKeyValueStore alloc] initWithCollection:@"kOWSProfileManager_UserUUIDWhitelistCollection"];
_whitelistedGroupsStore =
[[SDSKeyValueStore alloc] initWithCollection:@"kOWSProfileManager_GroupWhitelistCollection"];
_badgeStore = [[BadgeStore alloc] init];
OWSSingletonAssert();
AppReadinessRunNowOrWhenAppDidBecomeReadySync(^{
if (CurrentAppContext().isMainApp && !CurrentAppContext().isRunningTests
&& TSAccountManager.shared.isRegistered) {
[self logLocalAvatarStatus];
[self fetchLocalUsersProfile];
}
});
AppReadinessRunNowOrWhenAppDidBecomeReadyAsync(^{
if (TSAccountManager.shared.isRegistered) {
[self rotateLocalProfileKeyIfNecessary];
[OWSProfileManager updateProfileOnServiceIfNecessary];
[OWSProfileManager updateStorageServiceIfNecessary];
}
});
[self observeNotifications];
return self;
}
- (void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
- (void)observeNotifications
{
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(applicationDidBecomeActive:)
name:OWSApplicationDidBecomeActiveNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(reachabilityChanged:)
name:SSKReachability.owsReachabilityDidChange
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(blockListDidChange:)
name:BlockingManager.blockListDidChange
object:nil];
}
#pragma mark -
- (void)logLocalAvatarStatus
{
OWSAssertDebug(CurrentAppContext().isMainApp);
OWSAssertDebug(!CurrentAppContext().isRunningTests);
OWSAssertDebug(TSAccountManager.shared.isRegistered);
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[self logLocalAvatarStatus:self.localUserProfile label:@"cached copy"];
__block OWSUserProfile *_Nullable localUserProfile;
[self.databaseStorage readWithBlock:^(SDSAnyReadTransaction *transaction) {
localUserProfile = [OWSUserProfile getUserProfileForAddress:OWSUserProfile.localProfileAddress
transaction:transaction];
}];
[self logLocalAvatarStatus:localUserProfile label:@"database copy"];
});
}
- (void)logLocalAvatarStatus:(OWSUserProfile *_Nullable)localUserProfile label:(NSString *)label
{
if (localUserProfile == nil) {
OWSFailDebug(@"Missing local user profile: %@.", label);
return;
}
BOOL hasAvatarFileOnDisk = NO;
if (localUserProfile.avatarFileName.length > 0) {
NSString *filePath = [OWSUserProfile profileAvatarFilepathWithFilename:localUserProfile.avatarFileName];
hasAvatarFileOnDisk = [OWSFileSystem fileOrFolderExistsAtPath:filePath];
}
if (SSKDebugFlags.internalLogging) {
OWSLogInfo(
@"Local user profile (%@). address: %@, avatarUrlPath: %@, avatarFileName: %@, hasAvatarFileOnDisk: %d",
label,
localUserProfile.address,
localUserProfile.avatarUrlPath,
localUserProfile.avatarFileName,
hasAvatarFileOnDisk);
} else {
OWSLogInfo(
@"Local user profile (%@). address: %@, avatarUrlPath: %d, avatarFileName: %d, hasAvatarFileOnDisk: %d",
label,
localUserProfile.address,
localUserProfile.avatarUrlPath.length > 0,
localUserProfile.avatarFileName.length > 0,
hasAvatarFileOnDisk);
}
}
#pragma mark - User Profile Accessor
- (void)warmCaches
{
OWSAssertDebug(GRDBSchemaMigrator.areMigrationsComplete);
// Clear out so we re-initialize if we ever re-run the "on launch" logic,
// such as after a completed database transfer.
@synchronized(self) {
_localUserProfile = nil;
}
[self ensureLocalProfileCached];
}
- (void)ensureLocalProfileCached
{
// Since localUserProfile can create a transaction, we want to make sure it's not called for the first
// time unexpectedly (e.g. in a nested transaction.)
__unused OWSUserProfile *profile = [self localUserProfile];
}
#pragma mark - Local Profile
- (OWSUserProfile *)localUserProfile
{
OWSAssertDebug(GRDBSchemaMigrator.areMigrationsComplete);
@synchronized(self) {
if (_localUserProfile) {
OWSAssertDebug(_localUserProfile.profileKey);
return [_localUserProfile shallowCopy];
}
}
// We create the profile outside the @synchronized block
// to avoid any risk of deadlock. Thanks to ensureLocalProfileCached,
// there should be no risk of races. And races should be harmless
// since: a) getOrBuildUserProfileForAddress is idempotent and b) we use
// the "update with..." pattern.
//
// We first try using a read block to avoid opening a write block.
__block OWSUserProfile *_Nullable localUserProfile;
[self.databaseStorage readWithBlock:^(SDSAnyReadTransaction *transaction) {
localUserProfile = [self getLocalUserProfileWithTransaction:transaction];
} file:__FILE__ function:__FUNCTION__ line:__LINE__];
if (localUserProfile != nil) {
return [localUserProfile shallowCopy];
}
DatabaseStorageWrite(self.databaseStorage, ^(SDSAnyWriteTransaction *transaction) {
localUserProfile = [OWSUserProfile getOrBuildUserProfileForAddress:OWSUserProfile.localProfileAddress
transaction:transaction];
});
@synchronized(self) {
_localUserProfile = localUserProfile;
}
OWSAssertDebug(_localUserProfile.profileKey);
return [localUserProfile shallowCopy];
}
- (nullable OWSUserProfile *)getLocalUserProfileWithTransaction:(SDSAnyReadTransaction *)transaction
{
BOOL migrationsAreComplete = GRDBSchemaMigrator.areMigrationsComplete;
@synchronized(self) {
if (_localUserProfile && migrationsAreComplete) {
OWSAssertDebug(_localUserProfile.profileKey);
return [_localUserProfile shallowCopy];
}
}
OWSUserProfile *_Nullable localUserProfile =
[OWSUserProfile getUserProfileForAddress:OWSUserProfile.localProfileAddress transaction:transaction];
if (migrationsAreComplete) {
@synchronized(self) {
_localUserProfile = localUserProfile;
}
}
return [localUserProfile shallowCopy];
}
- (void)localProfileWasUpdated:(OWSUserProfile *)localUserProfile
{
OWSAssertDebug(GRDBSchemaMigrator.areMigrationsComplete);
@synchronized(self) {
_localUserProfile = [localUserProfile shallowCopy];
}
}
- (BOOL)localProfileExistsWithTransaction:(SDSAnyReadTransaction *)transaction
{
return [OWSUserProfile localUserProfileExistsWithTransaction:transaction];
}
- (OWSAES256Key *)localProfileKey
{
OWSAssertDebug(self.localUserProfile.profileKey.keyData.length == kAES256_KeyByteLength);
return self.localUserProfile.profileKey;
}
- (BOOL)hasLocalProfile
{
return (self.localGivenName.length > 0 || self.localProfileAvatarImage != nil);
}
- (BOOL)hasProfileName
{
return self.localGivenName.length > 0;
}
- (nullable NSString *)localGivenName
{
return self.localUserProfile.givenName;
}
- (nullable NSString *)localFamilyName
{
return self.localUserProfile.familyName;
}
- (nullable NSString *)localFullName
{
return self.localUserProfile.fullName;
}
- (nullable UIImage *)localProfileAvatarImage
{
return [self loadProfileAvatarWithFilename:self.localUserProfile.avatarFileName];
}
- (nullable NSData *)localProfileAvatarData
{
NSString *_Nullable filename = self.localUserProfile.avatarFileName;
if (filename.length < 1) {
return nil;
}
return [self loadProfileAvatarDataWithFilename:filename];
}
- (nullable NSString *)localUsername
{
return self.localUserProfile.username;
}
- (OWSProfileSnapshot *)localProfileSnapshotWithShouldIncludeAvatar:(BOOL)shouldIncludeAvatar
{
return [self profileSnapshotForUserProfile:self.localUserProfile shouldIncludeAvatar:shouldIncludeAvatar];
}
#ifdef DEBUG
- (void)logLocalProfile
{
OWSLogVerbose(@"Local profile: %@", self.localUserProfile.dictionaryValue);
}
#endif
- (OWSProfileSnapshot *)profileSnapshotForUserProfile:(OWSUserProfile *)userProfile
shouldIncludeAvatar:(BOOL)shouldIncludeAvatar
{
NSData *_Nullable avatarData = nil;
if (shouldIncludeAvatar && userProfile.avatarFileName.length > 0) {
avatarData = [self loadProfileAvatarDataWithFilename:userProfile.avatarFileName];
}
return [[OWSProfileSnapshot alloc] initWithGivenName:userProfile.givenName
familyName:userProfile.familyName
fullName:userProfile.fullName
bio:userProfile.bio
bioEmoji:userProfile.bioEmoji
username:userProfile.username
avatarData:avatarData
profileBadgeInfo:userProfile.profileBadgeInfo];
}
- (void)updateLocalUsername:(nullable NSString *)username
userProfileWriter:(UserProfileWriter)userProfileWriter
transaction:(SDSAnyWriteTransaction *)transaction
{
OWSAssertDebug(username == nil || username.length > 0);
OWSUserProfile *userProfile = self.localUserProfile;
OWSAssertDebug(self.localUserProfile);
[userProfile updateWithUsername:username
isUuidCapable:YES
userProfileWriter:userProfileWriter
transaction:transaction];
}
- (void)writeAvatarToDiskWithData:(NSData *)avatarData
success:(void (^)(NSString *fileName))successBlock
failure:(ProfileManagerFailureBlock)failureBlock
{
OWSAssertDebug(avatarData);
OWSAssertDebug(successBlock);
OWSAssertDebug(failureBlock);
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSString *filename = [self generateAvatarFilename];
NSString *filePath = [OWSUserProfile profileAvatarFilepathWithFilename:filename];
BOOL success = [avatarData writeToFile:filePath atomically:YES];
OWSAssertDebug(success);
if (success) {
return successBlock(filename);
}
failureBlock([OWSError withError:OWSErrorCodeAvatarWriteFailed
description:@"Avatar write failed."
isRetryable:NO]);
});
}
+ (NSData *)avatarDataForAvatarImage:(UIImage *)image
{
NSUInteger kMaxAvatarBytes = 5 * 1000 * 1000;
if (image.pixelWidth != kOWSProfileManager_MaxAvatarDiameterPixels
|| image.pixelHeight != kOWSProfileManager_MaxAvatarDiameterPixels) {
// To help ensure the user is being shown the same cropping of their avatar as
// everyone else will see, we want to be sure that the image was resized before this point.
OWSFailDebug(@"Avatar image should have been resized before trying to upload");
image = [image resizedImageToFillPixelSize:CGSizeMake(kOWSProfileManager_MaxAvatarDiameterPixels,
kOWSProfileManager_MaxAvatarDiameterPixels)];
}
NSData *_Nullable data = UIImageJPEGRepresentation(image, 0.95f);
if (data.length > kMaxAvatarBytes) {
// Our avatar dimensions are so small that it's incredibly unlikely we wouldn't be able to fit our profile
// photo. e.g. generating pure noise at our resolution compresses to ~200k.
OWSFailDebug(@"Surprised to find profile avatar was too large. Was it scaled properly? image: %@", image);
}
return data;
}
- (void)fetchLocalUsersProfile
{
SignalServiceAddress *_Nullable localAddress = self.tsAccountManager.localAddress;
if (!localAddress.isValid) {
return;
}
[self fetchProfileForAddress:localAddress];
}
- (void)fetchProfileForAddress:(SignalServiceAddress *)address
{
[ProfileFetcherJob fetchProfileWithAddress:address ignoreThrottling:YES];
}
- (AnyPromise *)fetchLocalUsersProfilePromise
{
SignalServiceAddress *_Nullable localAddress = self.tsAccountManager.localAddress;
if (!localAddress.isValid) {
return [AnyPromise promiseWithError:OWSErrorMakeAssertionError(@"Missing local address.")];
}
return [ProfileFetcherJob fetchProfilePromiseObjcWithAddress:localAddress mainAppOnly:NO ignoreThrottling:YES];
}
- (AnyPromise *)fetchProfileForAddressPromise:(SignalServiceAddress *)address
{
return [ProfileFetcherJob fetchProfilePromiseObjcWithAddress:address mainAppOnly:NO ignoreThrottling:YES];
}
- (AnyPromise *)fetchProfileForAddressPromise:(SignalServiceAddress *)address
mainAppOnly:(BOOL)mainAppOnly
ignoreThrottling:(BOOL)ignoreThrottling
{
return [ProfileFetcherJob fetchProfilePromiseObjcWithAddress:address
mainAppOnly:mainAppOnly
ignoreThrottling:ignoreThrottling];
}
- (void)fetchProfileForUsername:(NSString *)username
success:(void (^)(SignalServiceAddress *))success
notFound:(void (^)(void))notFound
failure:(void (^)(NSError *))failure
{
OWSAssertDebug(username.length > 0);
// Check if we have a cached profile for this username, if so avoid fetching it from the service
// since we are limited to 100 username lookups per day.
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
__block OWSUserProfile *_Nullable userProfile;
DatabaseStorageWrite(self.databaseStorage, ^(SDSAnyWriteTransaction *transaction) {
userProfile = [OWSUserProfile userProfileForUsername:username transaction:transaction];
});
if (userProfile) {
success(userProfile.publicAddress);
return;
}
[ProfileFetcherJob fetchProfileWithUsername:username success:success notFound:notFound failure:failure];
});
}
- (void)reuploadLocalProfile
{
[self reuploadLocalProfilePromise].done(^(id value) { OWSLogInfo(@"Done."); }).catch(^(NSError *error) {
OWSFailDebugUnlessNetworkFailure(error);
});
}
#pragma mark - Profile Key Rotation
- (NSString *)groupKeyForGroupId:(NSData *)groupId
{
return [groupId hexadecimalString];
}
- (nullable NSData *)groupIdForGroupKey:(NSString *)groupKey {
NSData *_Nullable groupId = [NSData dataFromHexString:groupKey];
// Make sure that the group id is a valid v1 or v2 group id.
if (![GroupManager isValidGroupIdOfAnyKind:groupId]) {
OWSFailDebug(@"Parsed group id has unexpected length: %@ (%lu)",
groupId.hexadecimalString,
(unsigned long)groupId.length);
return nil;
}
return [groupId copy];
}
- (void)rotateLocalProfileKeyIfNecessary {
if (CurrentAppContext().isNSE) {
return;
}
if (!self.tsAccountManager.isRegisteredPrimaryDevice) {
OWSAssertDebug(self.tsAccountManager.isRegistered);
OWSLogVerbose(@"Not rotating profile key on non-primary device");
return;
}
[self rotateLocalProfileKeyIfNecessaryWithSuccess:^{} failure:^(NSError *error) {}];
}
- (void)rotateLocalProfileKeyIfNecessaryWithSuccess:(dispatch_block_t)success
failure:(ProfileManagerFailureBlock)failure {
OWSAssertDebug(AppReadiness.isAppReady);
if (!self.tsAccountManager.isRegistered) {
OWSFailDebug(@"tsAccountManager.isRegistered was unexpectely false");
success();
return;
}
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSMutableSet<NSString *> *whitelistedPhoneNumbers = [NSMutableSet new];
NSMutableSet<NSString *> *whitelistedUUIDS = [NSMutableSet new];
NSMutableSet<NSData *> *whitelistedGroupIds = [NSMutableSet new];
[self.databaseStorage readWithBlock:^(SDSAnyReadTransaction *transaction) {
[whitelistedPhoneNumbers
addObjectsFromArray:[self.whitelistedPhoneNumbersStore allKeysWithTransaction:transaction]];
[whitelistedUUIDS addObjectsFromArray:[self.whitelistedUUIDsStore allKeysWithTransaction:transaction]];
NSArray<NSString *> *whitelistedGroupKeys =
[self.whitelistedGroupsStore allKeysWithTransaction:transaction];
for (NSString *groupKey in whitelistedGroupKeys) {
NSData *_Nullable groupId = [self groupIdForGroupKey:groupKey];
if (!groupId) {
OWSFailDebug(@"Couldn't parse group key: %@.", groupKey);
continue;
}
[whitelistedGroupIds addObject:groupId];
// Note: We don't add the group member's ids to whitelistedUUIDS
// and whitelistedPhoneNumbers.
//
// Whenever we message a contact, be it in a 1:1 thread or in a group thread,
// we add them to the contact whitelist, so there's no reason to redundantly
// add them here.
//
// Furthermore, doing so would cause the following problem:
//
// - Alice is in group Book Club
// - Add Book Club to your profile white list
// - Message Book Club, which also adds Alice to your profile whitelist.
// - Block Alice, but not Book Club
//
// Now, at this point we'd want to rotate our profile key once, since Alice has
// it via BookClub.
//
// The next time we checked whether we should rotate our profile key, adding all
// group members to whitelistedUUIDS and whitelistedPhoneNumbers would
// include Alice, and we'd rotate our profile key every time this method is called.
}
}];
SignalServiceAddress *_Nullable localAddress = [self.tsAccountManager localAddress];
NSString *_Nullable localNumber = localAddress.phoneNumber;
if (localNumber) {
[whitelistedPhoneNumbers removeObject:localNumber];
} else {
OWSFailDebug(@"Missing localNumber");
}
NSString *_Nullable localUUID = localAddress.uuidString;
if (localUUID) {
[whitelistedUUIDS removeObject:localUUID];
} else {
OWSFailDebug(@"Missing localUUID");
}
NSSet<NSString *> *blockedPhoneNumbers = self.blockingManager.blockedPhoneNumbers;
NSSet<NSString *> *blockedUUIDs = self.blockingManager.blockedUUIDStrings;
NSSet<NSData *> *blockedGroupIds = self.blockingManager.blockedGroupIds;
// Find the users and groups which are both a) blocked b) may have our current profile key.
NSMutableSet<NSString *> *intersectingPhoneNumbers = [blockedPhoneNumbers mutableCopy];
[intersectingPhoneNumbers intersectSet:whitelistedPhoneNumbers];
NSMutableSet<NSString *> *intersectingUUIDS = [blockedUUIDs mutableCopy];
[intersectingUUIDS intersectSet:whitelistedUUIDS];
NSMutableSet<NSData *> *intersectingGroupIds = [blockedGroupIds mutableCopy];
[intersectingGroupIds intersectSet:whitelistedGroupIds];
BOOL isProfileKeySharedWithBlocked
= (intersectingPhoneNumbers.count > 0 || intersectingUUIDS.count > 0 || intersectingGroupIds.count > 0);
if (!isProfileKeySharedWithBlocked) {
// No need to rotate the profile key.
return success();
}
[self rotateProfileKeyWithIntersectingPhoneNumbers:intersectingPhoneNumbers
intersectingUUIDs:intersectingUUIDS
intersectingGroupIds:intersectingGroupIds]
.done(^(id value) { success(); })
.catch(^(NSError *error) { failure(error); });
});
}
#pragma mark - Profile Whitelist
- (void)clearProfileWhitelist
{
OWSLogWarn(@"Clearing the profile whitelist.");
DatabaseStorageAsyncWrite(self.databaseStorage, ^(SDSAnyWriteTransaction *transaction) {
[self.whitelistedPhoneNumbersStore removeAllWithTransaction:transaction];
[self.whitelistedUUIDsStore removeAllWithTransaction:transaction];
[self.whitelistedGroupsStore removeAllWithTransaction:transaction];
OWSAssertDebug(0 == [self.whitelistedPhoneNumbersStore numberOfKeysWithTransaction:transaction]);
OWSAssertDebug(0 == [self.whitelistedUUIDsStore numberOfKeysWithTransaction:transaction]);
OWSAssertDebug(0 == [self.whitelistedGroupsStore numberOfKeysWithTransaction:transaction]);
});
}
// TODO: We could add a userProfileWriter parameter.
- (void)removeThreadFromProfileWhitelist:(TSThread *)thread
{
OWSLogWarn(@"Removing thread from profile whitelist: %@", thread);
DatabaseStorageWrite(self.databaseStorage, ^(SDSAnyWriteTransaction *transaction) {
if ([thread isKindOfClass:TSContactThread.class]) {
TSContactThread *contactThread = (TSContactThread *)thread;
[self removeUserFromProfileWhitelist:contactThread.contactAddress
userProfileWriter:UserProfileWriter_LocalUser
transaction:transaction];
} else {
TSGroupThread *groupThread = (TSGroupThread *)thread;
[self removeGroupIdFromProfileWhitelist:groupThread.groupModel.groupId
userProfileWriter:UserProfileWriter_LocalUser
transaction:transaction];
}
});
}
- (void)logProfileWhitelist
{
[self.databaseStorage readWithBlock:^(SDSAnyReadTransaction *transaction) {
OWSLogError(@"%@: %lu",
self.whitelistedPhoneNumbersStore.collection,
(unsigned long)[self.whitelistedPhoneNumbersStore numberOfKeysWithTransaction:transaction]);
for (NSString *key in [self.whitelistedPhoneNumbersStore allKeysWithTransaction:transaction]) {
OWSLogError(@"\t profile whitelist user phone number: %@", key);
}
OWSLogError(@"%@: %lu",
self.whitelistedUUIDsStore.collection,
(unsigned long)[self.whitelistedUUIDsStore numberOfKeysWithTransaction:transaction]);
for (NSString *key in [self.whitelistedUUIDsStore allKeysWithTransaction:transaction]) {
OWSLogError(@"\t profile whitelist user uuid: %@", key);
}
OWSLogError(@"%@: %lu",
self.whitelistedGroupsStore.collection,
(unsigned long)[self.whitelistedGroupsStore numberOfKeysWithTransaction:transaction]);
for (NSString *key in [self.whitelistedGroupsStore allKeysWithTransaction:transaction]) {
OWSLogError(@"\t profile whitelist group: %@", key);
}
}];
}
- (void)debug_regenerateLocalProfileWithSneakyTransaction
{
OWSUserProfile *userProfile = self.localUserProfile;
DatabaseStorageWrite(self.databaseStorage, ^(SDSAnyWriteTransaction *transaction) {
[userProfile clearWithProfileKey:[OWSAES256Key generateRandomKey]
userProfileWriter:UserProfileWriter_Debugging
transaction:transaction
completion:nil];
});
[self.tsAccountManager updateAccountAttributes].catch(^(NSError *error) {
OWSLogError(@"Error: %@.", error);
});
}
- (void)setLocalProfileKey:(OWSAES256Key *)key
userProfileWriter:(UserProfileWriter)userProfileWriter
transaction:(SDSAnyWriteTransaction *)transaction
{
OWSAssertDebug(GRDBSchemaMigrator.areMigrationsComplete);
OWSUserProfile *localUserProfile;
@synchronized(self)
{
// If it didn't exist, create it in a safe way as accessing the property
// will do a sneaky transaction.
if (!_localUserProfile) {
// We assert on the ivar directly here, as we want this to be cached already
// by the time this method is called. If it's not, we've changed our caching
// logic and should re-evalulate this method.
OWSFailDebug(@"Missing local profile when setting key.");
localUserProfile = [OWSUserProfile getOrBuildUserProfileForAddress:OWSUserProfile.localProfileAddress
transaction:transaction];
_localUserProfile = localUserProfile;
} else {
localUserProfile = _localUserProfile;
}
}
[localUserProfile updateWithProfileKey:key
userProfileWriter:userProfileWriter
transaction:transaction
completion:nil];
}
- (void)addUserToProfileWhitelist:(SignalServiceAddress *)address
{
OWSAssertDebug(address.isValid);
[self addUsersToProfileWhitelist:@[ address ]];
}
// TODO: We could add a userProfileWriter parameter.
- (void)addUsersToProfileWhitelist:(NSArray<SignalServiceAddress *> *)addresses
{
OWSAssertDebug(addresses);
// Try to avoid opening a write transaction.
AppReadinessRunNowOrWhenAppDidBecomeReadyAsync(^{
[self.databaseStorage asyncReadWithBlock:^(SDSAnyReadTransaction *readTransaction) {
NSSet<SignalServiceAddress *> *addressesToAdd = [self addressesNotBlockedOrInWhitelist:addresses
transaction:readTransaction];
if (addressesToAdd.count < 1) {
return;
}
DatabaseStorageAsyncWrite(self.databaseStorage, ^(SDSAnyWriteTransaction *writeTransaction) {
[self addConfirmedUnwhitelistedAddresses:addressesToAdd
userProfileWriter:UserProfileWriter_LocalUser
transaction:writeTransaction];
});
}];
});
}
- (void)addUserToProfileWhitelist:(SignalServiceAddress *)address
userProfileWriter:(UserProfileWriter)userProfileWriter
transaction:(SDSAnyWriteTransaction *)transaction
{
OWSAssertDebug(address.isValid);
OWSAssertDebug(transaction);
NSSet *addressesToAdd = [self addressesNotBlockedOrInWhitelist:@[ address ] transaction:transaction];
[self addConfirmedUnwhitelistedAddresses:addressesToAdd
userProfileWriter:userProfileWriter
transaction:transaction];
}
- (void)addUsersToProfileWhitelist:(NSArray<SignalServiceAddress *> *)addresses
userProfileWriter:(UserProfileWriter)userProfileWriter
transaction:(SDSAnyWriteTransaction *)transaction
{
OWSAssertDebug(addresses);
OWSAssertDebug(transaction);
NSSet<SignalServiceAddress *> *addressesToAdd = [self addressesNotBlockedOrInWhitelist:addresses
transaction:transaction];
if (addressesToAdd.count < 1) {
return;
}
[self addConfirmedUnwhitelistedAddresses:addressesToAdd
userProfileWriter:userProfileWriter
transaction:transaction];
}
- (void)removeUserFromProfileWhitelist:(SignalServiceAddress *)address
{
OWSAssertDebug(address.isValid);
[self removeUsersFromProfileWhitelist:@[ address ]];
}
- (void)removeUserFromProfileWhitelist:(SignalServiceAddress *)address
userProfileWriter:(UserProfileWriter)userProfileWriter
transaction:(SDSAnyWriteTransaction *)transaction
{
OWSAssertDebug(address.isValid);
OWSAssertDebug(transaction);
NSSet *addressesToRemove = [self addressesInWhitelist:@[ address ] transaction:transaction];
[self removeConfirmedWhitelistedAddresses:addressesToRemove
userProfileWriter:userProfileWriter
transaction:transaction];
}
// TODO: We could add a userProfileWriter parameter.
- (void)removeUsersFromProfileWhitelist:(NSArray<SignalServiceAddress *> *)addresses
{
OWSAssertDebug(addresses);
// Try to avoid opening a write transaction.
[self.databaseStorage asyncReadWithBlock:^(SDSAnyReadTransaction *readTransaction) {
NSSet<SignalServiceAddress *> *addressesToRemove = [self addressesInWhitelist:addresses
transaction:readTransaction];
if (addressesToRemove.count < 1) {
return;
}
DatabaseStorageAsyncWrite(self.databaseStorage, ^(SDSAnyWriteTransaction *writeTransaction) {
[self removeConfirmedWhitelistedAddresses:addressesToRemove
userProfileWriter:UserProfileWriter_LocalUser
transaction:writeTransaction];
});
}];
}
- (NSSet<SignalServiceAddress *> *)addressesNotBlockedOrInWhitelist:(NSArray<SignalServiceAddress *> *)addresses
transaction:(SDSAnyReadTransaction *)transaction
{
OWSAssertDebug(addresses);
NSMutableSet<SignalServiceAddress *> *notBlockedOrInWhitelist = [NSMutableSet new];
for (SignalServiceAddress *address in addresses) {
// If the address is blocked, we don't want to include it
if ([self.blockingManager isAddressBlocked:address]) {
continue;
}
// We want to include both the UUID and the phone number in the white list.
// It's possible we white listed one but not both, so we check each.
BOOL notInWhitelist = NO;
if (address.uuidString) {
BOOL currentlyWhitelisted = [self.whitelistedUUIDsStore hasValueForKey:address.uuidString
transaction:transaction];
if (!currentlyWhitelisted) {
notInWhitelist = YES;
}
}
if (address.phoneNumber) {
BOOL currentlyWhitelisted = [self.whitelistedPhoneNumbersStore hasValueForKey:address.phoneNumber
transaction:transaction];
if (!currentlyWhitelisted) {
notInWhitelist = YES;
}
}
if (notInWhitelist) {
[notBlockedOrInWhitelist addObject:address];
}
}
return [notBlockedOrInWhitelist copy];
}
- (NSSet<SignalServiceAddress *> *)addressesInWhitelist:(NSArray<SignalServiceAddress *> *)addresses
transaction:(SDSAnyReadTransaction *)transaction
{
OWSAssertDebug(addresses);
NSMutableSet<SignalServiceAddress *> *whitelistedAddresses = [NSMutableSet new];
for (SignalServiceAddress *address in addresses) {
// We only consider an address whitelisted if either the UUID and phone
// number are represented. It's possible we white listed one but not both,
// so we check each.
BOOL isInWhitelist = NO;
if (address.uuidString) {
BOOL currentlyWhitelisted = [self.whitelistedUUIDsStore hasValueForKey:address.uuidString
transaction:transaction];
if (currentlyWhitelisted) {
isInWhitelist = YES;
}
}
if (address.phoneNumber) {
BOOL currentlyWhitelisted = [self.whitelistedPhoneNumbersStore hasValueForKey:address.phoneNumber
transaction:transaction];
if (currentlyWhitelisted) {
isInWhitelist = YES;
}
}
if (isInWhitelist) {
[whitelistedAddresses addObject:address];
}
}
return [whitelistedAddresses copy];
}
- (void)removeConfirmedWhitelistedAddresses:(NSSet<SignalServiceAddress *> *)addressesToRemove
userProfileWriter:(UserProfileWriter)userProfileWriter
transaction:(SDSAnyWriteTransaction *)transaction
{
if (addressesToRemove.count == 0) {
// Do nothing.
return;
}
for (SignalServiceAddress *address in addressesToRemove) {
if (address.uuidString) {
[self.whitelistedUUIDsStore removeValueForKey:address.uuidString transaction:transaction];
}
if (address.phoneNumber) {
[self.whitelistedPhoneNumbersStore removeValueForKey:address.phoneNumber transaction:transaction];
}
TSThread *_Nullable thread = [TSContactThread getThreadWithContactAddress:address transaction:transaction];
if (thread) {
[self.databaseStorage touchThread:thread shouldReindex:NO transaction:transaction];
}
}
[transaction addSyncCompletion:^{
// Mark the removed whitelisted addresses for update
if (shouldUpdateStorageServiceForUserProfileWriter(userProfileWriter)) {
[self.storageServiceManager recordPendingUpdatesWithUpdatedAddresses:addressesToRemove.allObjects];
}
for (SignalServiceAddress *address in addressesToRemove) {
[[NSNotificationCenter defaultCenter]
postNotificationNameAsync:kNSNotificationNameProfileWhitelistDidChange
object:nil
userInfo:@{
kNSNotificationKey_ProfileAddress : address,
kNSNotificationKey_UserProfileWriter : @(userProfileWriter),
}];
}
}];
}
- (void)addConfirmedUnwhitelistedAddresses:(NSSet<SignalServiceAddress *> *)addressesToAdd
userProfileWriter:(UserProfileWriter)userProfileWriter
transaction:(SDSAnyWriteTransaction *)transaction
{
if (addressesToAdd.count == 0) {
// Do nothing.
return;
}
for (SignalServiceAddress *address in addressesToAdd) {
if (address.uuidString) {
[self.whitelistedUUIDsStore setBool:YES key:address.uuidString transaction:transaction];
}
if (address.phoneNumber) {
[self.whitelistedPhoneNumbersStore setBool:YES key:address.phoneNumber transaction:transaction];
}
TSThread *_Nullable thread = [TSContactThread getThreadWithContactAddress:address transaction:transaction];
if (thread) {
[self.databaseStorage touchThread:thread shouldReindex:NO transaction:transaction];
}
}
[transaction addSyncCompletion:^{
// Mark the new whitelisted addresses for update
if (shouldUpdateStorageServiceForUserProfileWriter(userProfileWriter)) {
[self.storageServiceManager recordPendingUpdatesWithUpdatedAddresses:addressesToAdd.allObjects];
}
for (SignalServiceAddress *address in addressesToAdd) {
[[NSNotificationCenter defaultCenter]
postNotificationNameAsync:kNSNotificationNameProfileWhitelistDidChange
object:nil
userInfo:@{
kNSNotificationKey_ProfileAddress : address,
kNSNotificationKey_UserProfileWriter : @(userProfileWriter),
}];
}
}];
}
- (BOOL)isUserInProfileWhitelist:(SignalServiceAddress *)address transaction:(SDSAnyReadTransaction *)transaction
{
OWSAssertDebug(address.isValid);
// isAddressBlocked can open a sneaky transaction in
// BlockingManager.ensureLazyInitialization(), but we avoid this
// by ensuring that BlockingManager.warmCaches() is always
// called first, immediately after registering the database views.
if ([self.blockingManager isAddressBlocked:address]) {
return NO;
}
BOOL result = NO;
if (address.uuidString) {
result = [self.whitelistedUUIDsStore hasValueForKey:address.uuidString transaction:transaction];
}
if (!result && address.phoneNumber) {
result = [self.whitelistedPhoneNumbersStore hasValueForKey:address.phoneNumber transaction:transaction];
}
return result;
}
// TODO: We could add a userProfileWriter parameter.
- (void)addGroupIdToProfileWhitelist:(NSData *)groupId
{
OWSAssertDebug(groupId.length > 0);
NSString *groupIdKey = [self groupKeyForGroupId:groupId];
// Try to avoid opening a write transaction.
[self.databaseStorage asyncReadWithBlock:^(SDSAnyReadTransaction *readTransaction) {
if ([self.whitelistedGroupsStore hasValueForKey:groupIdKey transaction:readTransaction]) {
// Do nothing.
return;
}
DatabaseStorageAsyncWrite(self.databaseStorage, ^(SDSAnyWriteTransaction *writeTransaction) {
[self addConfirmedUnwhitelistedGroupId:groupId
userProfileWriter:UserProfileWriter_LocalUser
transaction:writeTransaction];
});
}];
}
- (void)addGroupIdToProfileWhitelist:(NSData *)groupId
userProfileWriter:(UserProfileWriter)userProfileWriter
transaction:(SDSAnyWriteTransaction *)transaction
{
OWSAssertDebug(groupId.length > 0);
NSString *groupIdKey = [self groupKeyForGroupId:groupId];
if (![self.whitelistedGroupsStore hasValueForKey:groupIdKey transaction:transaction]) {
[self addConfirmedUnwhitelistedGroupId:groupId userProfileWriter:userProfileWriter transaction:transaction];
}
}
// TODO: We could add a userProfileWriter parameter.
- (void)removeGroupIdFromProfileWhitelist:(NSData *)groupId
{
OWSAssertDebug(groupId.length > 0);
NSString *groupIdKey = [self groupKeyForGroupId:groupId];
// Try to avoid opening a write transaction.
[self.databaseStorage asyncReadWithBlock:^(SDSAnyReadTransaction *readTransaction) {
if (![self.whitelistedGroupsStore hasValueForKey:groupIdKey transaction:readTransaction]) {
// Do nothing.
return;
}
DatabaseStorageAsyncWrite(self.databaseStorage, ^(SDSAnyWriteTransaction *writeTransaction) {
[self removeConfirmedWhitelistedGroupId:groupId
userProfileWriter:UserProfileWriter_LocalUser
transaction:writeTransaction];
});
}];
}
- (void)removeGroupIdFromProfileWhitelist:(NSData *)groupId
userProfileWriter:(UserProfileWriter)userProfileWriter
transaction:(SDSAnyWriteTransaction *)transaction
{
OWSAssertDebug(groupId.length > 0);
NSString *groupIdKey = [self groupKeyForGroupId:groupId];
if ([self.whitelistedGroupsStore hasValueForKey:groupIdKey transaction:transaction]) {
[self removeConfirmedWhitelistedGroupId:groupId userProfileWriter:userProfileWriter transaction:transaction];
}
}
- (void)removeConfirmedWhitelistedGroupId:(NSData *)groupId
userProfileWriter:(UserProfileWriter)userProfileWriter
transaction:(SDSAnyWriteTransaction *)transaction
{
OWSAssertDebug(groupId.length > 0);
NSString *groupIdKey = [self groupKeyForGroupId:groupId];
[self.whitelistedGroupsStore removeValueForKey:groupIdKey transaction:transaction];
[TSGroupThread ensureGroupIdMappingForGroupId:groupId transaction:transaction];
TSThread *_Nullable groupThread = [TSGroupThread fetchWithGroupId:groupId transaction:transaction];
if (groupThread) {
[self.databaseStorage touchThread:groupThread shouldReindex:NO transaction:transaction];
}
[transaction addSyncCompletion:^{
// Mark the group for update
if (shouldUpdateStorageServiceForUserProfileWriter(userProfileWriter)) {
[self recordPendingUpdatesForStorageServiceWithGroupId:groupId];
}
[[NSNotificationCenter defaultCenter]
postNotificationNameAsync:kNSNotificationNameProfileWhitelistDidChange
object:nil
userInfo:@{
kNSNotificationKey_ProfileGroupId : groupId,
kNSNotificationKey_UserProfileWriter : @(userProfileWriter),
}];
}];
}
- (void)addConfirmedUnwhitelistedGroupId:(NSData *)groupId
userProfileWriter:(UserProfileWriter)userProfileWriter
transaction:(SDSAnyWriteTransaction *)transaction
{
OWSAssertDebug(groupId.length > 0);
NSString *groupIdKey = [self groupKeyForGroupId:groupId];
[self.whitelistedGroupsStore setBool:YES key:groupIdKey transaction:transaction];
[TSGroupThread ensureGroupIdMappingForGroupId:groupId transaction:transaction];
TSThread *_Nullable groupThread = [TSGroupThread fetchWithGroupId:groupId transaction:transaction];
if (groupThread) {
[self.databaseStorage touchThread:groupThread shouldReindex:NO transaction:transaction];
}
[transaction addSyncCompletion:^{
// Mark the group for update
if (shouldUpdateStorageServiceForUserProfileWriter(userProfileWriter)) {
[self recordPendingUpdatesForStorageServiceWithGroupId:groupId];
}
[[NSNotificationCenter defaultCenter]
postNotificationNameAsync:kNSNotificationNameProfileWhitelistDidChange
object:nil
userInfo:@{
kNSNotificationKey_ProfileGroupId : groupId,
kNSNotificationKey_UserProfileWriter : @(userProfileWriter),
}];
}];
}
- (void)recordPendingUpdatesForStorageServiceWithGroupId:(NSData *)groupId
{
OWSAssertDebug(groupId.length > 0);
[self.databaseStorage asyncReadWithBlock:^(SDSAnyReadTransaction *transaction) {
TSGroupThread *_Nullable groupThread = [TSGroupThread fetchWithGroupId:groupId transaction:transaction];
if (groupThread == nil) {
OWSFailDebug(@"Missing groupThread.");
return;
}
[self.storageServiceManager recordPendingUpdatesWithGroupModel:groupThread.groupModel];
}];
}
- (void)addThreadToProfileWhitelist:(TSThread *)thread
{
OWSAssertDebug(thread);
if (thread.isGroupThread) {
TSGroupThread *groupThread = (TSGroupThread *)thread;
NSData *groupId = groupThread.groupModel.groupId;
[self addGroupIdToProfileWhitelist:groupId];
} else {
TSContactThread *contactThread = (TSContactThread *)thread;
[self addUserToProfileWhitelist:contactThread.contactAddress];
}
}
// TODO: We could add a userProfileWriter parameter.
- (void)addThreadToProfileWhitelist:(TSThread *)thread transaction:(SDSAnyWriteTransaction *)transaction
{
OWSAssertDebug(thread);
if (thread.isGroupThread) {
TSGroupThread *groupThread = (TSGroupThread *)thread;
NSData *groupId = groupThread.groupModel.groupId;
[self addGroupIdToProfileWhitelist:groupId
userProfileWriter:UserProfileWriter_LocalUser
transaction:transaction];
} else {
TSContactThread *contactThread = (TSContactThread *)thread;
[self addUserToProfileWhitelist:contactThread.contactAddress
userProfileWriter:UserProfileWriter_LocalUser
transaction:transaction];
}
}
- (BOOL)isGroupIdInProfileWhitelist:(NSData *)groupId transaction:(SDSAnyReadTransaction *)transaction
{
OWSAssertDebug(groupId.length > 0);
// isGroupIdBlocked can open a sneaky transaction in
// BlockingManager.ensureLazyInitialization(), but we avoid this
// by ensuring that BlockingManager.warmCaches() is always
// called first. I've added asserts within BlockingManager around
// this.
if ([self.blockingManager isGroupIdBlocked:groupId]) {
return NO;
}
NSString *groupIdKey = [self groupKeyForGroupId:groupId];
return [self.whitelistedGroupsStore hasValueForKey:groupIdKey transaction:transaction];
}
- (BOOL)isThreadInProfileWhitelist:(TSThread *)thread transaction:(SDSAnyReadTransaction *)transaction
{
OWSAssertDebug(thread);
if (thread.isGroupThread) {
TSGroupThread *groupThread = (TSGroupThread *)thread;
NSData *groupId = groupThread.groupModel.groupId;
return [self isGroupIdInProfileWhitelist:groupId transaction:transaction];
} else {
TSContactThread *contactThread = (TSContactThread *)thread;
return [self isUserInProfileWhitelist:contactThread.contactAddress transaction:transaction];
}
}
- (void)setContactAddresses:(NSArray<SignalServiceAddress *> *)contactAddresses
{
OWSAssertDebug(contactAddresses);
[self addUsersToProfileWhitelist:contactAddresses];
}
#pragma mark - Other User's Profiles
- (void)logUserProfiles
{
[self.databaseStorage asyncReadWithBlock:^(SDSAnyReadTransaction *transaction) {
OWSLogError(@"logUserProfiles: %ld", (unsigned long)[OWSUserProfile anyCountWithTransaction:transaction]);
[OWSUserProfile
anyEnumerateWithTransaction:transaction
batched:YES
block:^(OWSUserProfile *userProfile, BOOL *stop) {
OWSLogError(@"\t [%@]: has profile key: %d, has avatar URL: %d, has "
@"avatar file: %d, given name: %@, family name: %@, username: %@, badges: %@",
userProfile.publicAddress,
userProfile.profileKey != nil,
userProfile.avatarUrlPath != nil,
userProfile.avatarFileName != nil,
userProfile.givenName,
userProfile.familyName,
userProfile.username,
userProfile.profileBadgeInfo);
}];
}];
}
- (void)setProfileKeyData:(NSData *)profileKeyData
forAddress:(SignalServiceAddress *)address
userProfileWriter:(UserProfileWriter)userProfileWriter
transaction:(SDSAnyWriteTransaction *)transaction
{
[self setProfileKeyData:profileKeyData
forAddress:address
onlyFillInIfMissing:NO
userProfileWriter:userProfileWriter
transaction:transaction];
}
- (void)setProfileKeyData:(NSData *)profileKeyData
forAddress:(SignalServiceAddress *)addressParam
onlyFillInIfMissing:(BOOL)onlyFillInIfMissing
userProfileWriter:(UserProfileWriter)userProfileWriter
transaction:(SDSAnyWriteTransaction *)transaction
{
SignalServiceAddress *address = [OWSUserProfile resolveUserProfileAddress:addressParam];
OWSAES256Key *_Nullable profileKey = [OWSAES256Key keyWithData:profileKeyData];
if (profileKey == nil) {
OWSFailDebug(@"Failed to make profile key for key data");
return;
}
OWSUserProfile *userProfile = [OWSUserProfile getOrBuildUserProfileForAddress:address transaction:transaction];
OWSAssertDebug(userProfile);
if (onlyFillInIfMissing && userProfile.profileKey != nil) {
return;
}
if (userProfile.profileKey && [userProfile.profileKey.keyData isEqual:profileKey.keyData]) {
// Ignore redundant update.
return;
}
// Whenever a user's profile key changes, we need to fetch a new
// profile key credential for them.
[self.versionedProfiles clearProfileKeyCredentialForAddress:address transaction:transaction];
[userProfile updateWithProfileKey:profileKey
userProfileWriter:userProfileWriter
transaction:transaction
completion:^{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// If this is the profile for the local user, we always want to defer to local state
// so skip the update profile for address call.
if ([OWSUserProfile isLocalProfileAddress:address]) {
return;
}
[self.udManager setUnidentifiedAccessMode:UnidentifiedAccessModeUnknown
address:address];
[self fetchProfileForAddress:address];
});
}];
}
- (void)fillInMissingProfileKeys:(NSDictionary<SignalServiceAddress *, NSData *> *)profileKeys
userProfileWriter:(UserProfileWriter)userProfileWriter
{
DatabaseStorageAsyncWrite(self.databaseStorage, ^(SDSAnyWriteTransaction *transaction) {
for (SignalServiceAddress *address in profileKeys) {
NSData *_Nullable profileKeyData = profileKeys[address];
if (profileKeyData == nil) {
OWSFailDebug(@"Missing profileKeyData.");
continue;
}
OWSAES256Key *_Nullable key = [OWSAES256Key keyWithData:profileKeyData];
if (key == nil) {
OWSFailDebug(@"Invalid profileKeyData.");
continue;
}
NSData *_Nullable existingProfileKeyData = [self profileKeyDataForAddress:address transaction:transaction];
if ([NSObject isNullableObject:existingProfileKeyData equalTo:profileKeyData]) {
// Redundant profileKeyData; no need to update.
continue;
}
OWSLogInfo(@"Filling in missing profile key for: %@", address);
[self setProfileKeyData:profileKeyData
forAddress:address
onlyFillInIfMissing:YES
userProfileWriter:userProfileWriter
transaction:transaction];
}
});
}
- (void)setProfileGivenName:(nullable NSString *)givenName
familyName:(nullable NSString *)familyName
forAddress:(SignalServiceAddress *)addressParam
userProfileWriter:(UserProfileWriter)userProfileWriter
transaction:(SDSAnyWriteTransaction *)transaction
{
SignalServiceAddress *address = [OWSUserProfile resolveUserProfileAddress:addressParam];
OWSAssertDebug(address.isValid);
OWSUserProfile *userProfile = [OWSUserProfile getOrBuildUserProfileForAddress:address transaction:transaction];
[userProfile updateWithGivenName:givenName
familyName:familyName
userProfileWriter:userProfileWriter
transaction:transaction
completion:nil];
}
- (void)setProfileGivenName:(nullable NSString *)givenName
familyName:(nullable NSString *)familyName
avatarUrlPath:(nullable NSString *)avatarUrlPath
forAddress:(SignalServiceAddress *)addressParam
userProfileWriter:(UserProfileWriter)userProfileWriter
transaction:(SDSAnyWriteTransaction *)transaction
{
SignalServiceAddress *address = [OWSUserProfile resolveUserProfileAddress:addressParam];
OWSAssertDebug(address.isValid);
OWSUserProfile *userProfile = [OWSUserProfile getOrBuildUserProfileForAddress:address transaction:transaction];
[userProfile updateWithGivenName:givenName
familyName:familyName
avatarUrlPath:avatarUrlPath
userProfileWriter:userProfileWriter
transaction:transaction
completion:nil];
if (userProfile.avatarUrlPath.length > 0 && userProfile.avatarFileName.length < 1) {
[self downloadAvatarForUserProfile:userProfile];
}
}
- (nullable NSData *)profileKeyDataForAddress:(SignalServiceAddress *)address
transaction:(SDSAnyReadTransaction *)transaction
{
return [self profileKeyForAddress:address transaction:transaction].keyData;
}
- (BOOL)recipientAddressIsUuidCapable:(nonnull SignalServiceAddress *)address
transaction:(nonnull SDSAnyReadTransaction *)transaction
{
OWSUserProfile *_Nullable userProfile = [OWSUserProfile getUserProfileForAddress:address transaction:transaction];
if (userProfile == nil) {
return NO;
} else {
return userProfile.isUuidCapable;
}
}
- (nullable OWSAES256Key *)profileKeyForAddress:(SignalServiceAddress *)address
transaction:(SDSAnyReadTransaction *)transaction
{
OWSAssertDebug(address.isValid);
OWSUserProfile *_Nullable userProfile = [self getUserProfileForAddress:address transaction:transaction];
return userProfile.profileKey;
}
- (nullable NSString *)unfilteredGivenNameForAddress:(SignalServiceAddress *)address
transaction:(SDSAnyReadTransaction *)transaction
{
OWSAssertDebug(address.isValid);
OWSUserProfile *_Nullable userProfile = [self getUserProfileForAddress:address transaction:transaction];
return userProfile.unfilteredGivenName;
}
- (nullable NSString *)givenNameForAddress:(SignalServiceAddress *)address
transaction:(SDSAnyReadTransaction *)transaction
{
OWSAssertDebug(address.isValid);
OWSUserProfile *_Nullable userProfile = [self getUserProfileForAddress:address transaction:transaction];
return userProfile.givenName;
}
- (nullable NSString *)unfilteredFamilyNameForAddress:(SignalServiceAddress *)address
transaction:(SDSAnyReadTransaction *)transaction
{
OWSAssertDebug(address.isValid);
OWSUserProfile *_Nullable userProfile = [self getUserProfileForAddress:address transaction:transaction];
return userProfile.unfilteredFamilyName;
}
- (nullable NSString *)familyNameForAddress:(SignalServiceAddress *)address
transaction:(SDSAnyReadTransaction *)transaction
{
OWSAssertDebug(address.isValid);
OWSUserProfile *_Nullable userProfile = [self getUserProfileForAddress:address transaction:transaction];
return userProfile.familyName;
}
- (nullable NSPersonNameComponents *)nameComponentsForAddress:(SignalServiceAddress *)address
transaction:(SDSAnyReadTransaction *)transaction
{
OWSAssertDebug(address.isValid);
OWSUserProfile *_Nullable userProfile = [self getUserProfileForAddress:address transaction:transaction];
return userProfile.nameComponents;
}
- (nullable NSString *)fullNameForAddress:(SignalServiceAddress *)address
transaction:(SDSAnyReadTransaction *)transaction
{
OWSAssertDebug(address.isValid);
OWSUserProfile *_Nullable userProfile = [self getUserProfileForAddress:address transaction:transaction];
return userProfile.fullName;
}
- (NSArray<id<SSKMaybeString>> *)fullNamesForAddresses:(NSArray<SignalServiceAddress *> *)addresses
transaction:(SDSAnyReadTransaction *)transaction
{
return [self objc_fullNamesForAddresses:addresses transaction:transaction];
}
- (nullable UIImage *)profileAvatarForAddress:(SignalServiceAddress *)address
transaction:(SDSAnyReadTransaction *)transaction
{
OWSAssertDebug(address.isValid);
OWSUserProfile *_Nullable userProfile = [self getUserProfileForAddress:address transaction:transaction];
if (userProfile.avatarFileName.length > 0) {
return [self loadProfileAvatarWithFilename:userProfile.avatarFileName];
}
if (userProfile.avatarUrlPath.length > 0) {
// Try to fill in missing avatar.
[self downloadAvatarForUserProfile:userProfile];
}
return nil;
}
- (BOOL)hasProfileAvatarData:(SignalServiceAddress *)address transaction:(SDSAnyReadTransaction *)transaction
{
OWSUserProfile *_Nullable userProfile = [self getUserProfileForAddress:address transaction:transaction];
if (userProfile.avatarFileName.length < 1) {
return NO;
} else {
NSString *filePath = [OWSUserProfile profileAvatarFilepathWithFilename:userProfile.avatarFileName];
return [OWSFileSystem fileOrFolderExistsAtPath:filePath];
}
}
- (nullable NSData *)profileAvatarDataForAddress:(SignalServiceAddress *)address
transaction:(SDSAnyReadTransaction *)transaction
{
OWSAssertDebug(address.isValid);
OWSUserProfile *_Nullable userProfile = [self getUserProfileForAddress:address transaction:transaction];
if (userProfile.avatarFileName.length > 0) {
return [self loadProfileAvatarDataWithFilename:userProfile.avatarFileName];
} else {
OWSLogWarn(@"Failed to get user profile to generate avatar data for %@. Has profile? %i. Has filename? %i",
address,
userProfile != nil,
userProfile.avatarFileName != nil);
}
return nil;
}
- (nullable NSString *)profileAvatarURLPathForAddress:(SignalServiceAddress *)address
transaction:(SDSAnyReadTransaction *)transaction
{
OWSAssertDebug(address.isValid);
OWSUserProfile *_Nullable userProfile = [self getUserProfileForAddress:address transaction:transaction];
if (userProfile.avatarUrlPath.length > 0 && userProfile.avatarFileName.length == 0) {
// Try to fill in missing avatar.
[self downloadAvatarForUserProfile:userProfile];
}
return userProfile.avatarUrlPath;
}
- (nullable NSString *)usernameForAddress:(SignalServiceAddress *)address
transaction:(SDSAnyReadTransaction *)transaction
{
OWSUserProfile *_Nullable userProfile = [self getUserProfileForAddress:address transaction:transaction];
if (userProfile.username.length > 0) {
return userProfile.username;
}
return nil;
}
- (nullable NSString *)profileBioForDisplayForAddress:(SignalServiceAddress *)address
transaction:(SDSAnyReadTransaction *)transaction
{
OWSAssertDebug(address.isValid);
OWSUserProfile *_Nullable userProfile = [self getUserProfileForAddress:address transaction:transaction];
return [OWSUserProfile bioForDisplayWithBio:userProfile.bio bioEmoji:userProfile.bioEmoji];
}
- (nullable OWSUserProfile *)getUserProfileForAddress:(SignalServiceAddress *)addressParam
transaction:(SDSAnyReadTransaction *)transaction
{
SignalServiceAddress *address = [OWSUserProfile resolveUserProfileAddress:addressParam];
OWSAssertDebug(address.isValid);
// For "local reads", use the local user profile.
if ([OWSUserProfile isLocalProfileAddress:address]) {
return [self getLocalUserProfileWithTransaction:transaction];
}
return [self.modelReadCaches.userProfileReadCache getUserProfileWithAddress:address transaction:transaction];
}
- (nullable NSURL *)writeAvatarDataToFile:(NSData *)avatarData
{
OWSAssertDebug(avatarData.length > 0);
if (![avatarData ows_isValidImage]) {
OWSFailDebug(@"Invalid avatar format");
return nil;
}
NSString *filename = [self generateAvatarFilename];
NSString *avatarPath = [OWSUserProfile profileAvatarFilepathWithFilename:filename];
NSURL *avatarUrl = [NSURL fileURLWithPath:avatarPath];
if (!avatarUrl) {
OWSFailDebug(@"Invalid URL for avatarPath %@", avatarPath);
return nil;
}
NSError *error = nil;
BOOL success = [avatarData writeToURL:avatarUrl options:NSDataWritingAtomic error:&error];
if (!success || error) {
OWSFailDebug(@"Failed write to url %@: %@", avatarUrl, error);
return nil;
}
// We were double checking that a UIImage could be instantiated from this file before recording the
// avatar to the profile. That behavior is preserved here:
UIImage *_Nullable avatarImage = [UIImage imageWithContentsOfFile:avatarUrl.path];
if (avatarImage) {
return avatarUrl;
} else {
OWSFailDebug(@"Failed to open avatar image written to disk");
return nil;
}
}
- (NSString *)generateAvatarFilename
{
return [[NSUUID UUID].UUIDString stringByAppendingPathExtension:@"jpg"];
}
// We may know a profile's avatar URL (avatarUrlPath != nil) but not
// have downloaded the avatar data yet (avatarFileName == nil).
// We use this method to fill in these missing avatars.
- (void)downloadAvatarForUserProfile:(OWSUserProfile *)userProfile
{
OWSAssertDebug(userProfile);
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
__block OWSBackgroundTask *backgroundTask = [OWSBackgroundTask backgroundTaskWithLabelStr:__PRETTY_FUNCTION__];
// Record the avatarUrlPath and profileKey; if they change
// during the avatar download, we don't want to update the profile.
__block NSString *_Nullable avatarUrlPathAtStart;
__block OWSAES256Key *_Nullable profileKeyAtStart;
__block BOOL shouldDownload;
[self.databaseStorage readWithBlock:^(SDSAnyReadTransaction *transaction) {
OWSUserProfile *_Nullable currentUserProfile = [OWSUserProfile getUserProfileForAddress:userProfile.address
transaction:transaction];
if (currentUserProfile == nil) {
OWSFailDebug(@"Aborting; currentUserProfile cannot be found.");
shouldDownload = NO;
return;
}
avatarUrlPathAtStart = currentUserProfile.avatarUrlPath;
profileKeyAtStart = currentUserProfile.profileKey;
if (profileKeyAtStart.keyData.length < 1 || avatarUrlPathAtStart.length < 1) {
OWSLogVerbose(@"Aborting; avatarUrlPath or profileKey are not known.");
shouldDownload = NO;
return;
}
if (currentUserProfile.avatarFileName.length > 0) {
OWSLogVerbose(@"Aborting; avatar already present.");
shouldDownload = NO;
return;
}
shouldDownload = YES;
}];
if (!shouldDownload) {
return;
}
NSString *filename = [self generateAvatarFilename];
NSString *filePath = [OWSUserProfile profileAvatarFilepathWithFilename:filename];
// downloadAndDecryptProfileAvatarForProfileAddress:... ensures that
// only one download is in flight at a time for a given avatar.
[self downloadAndDecryptProfileAvatarForProfileAddress:userProfile.address
avatarUrlPath:avatarUrlPathAtStart
profileKey:profileKeyAtStart]
.doneInBackground(^(id value) {
if (![value isKindOfClass:[NSData class]]) {
OWSFailDebug(@"Invalid value.");
return;
}
NSData *decryptedData = value;
BOOL success = [decryptedData writeToFile:filePath atomically:YES];
if (!success) {
OWSFailDebug(@"Could not write avatar to disk.");
return;
}
UIImage *_Nullable image = [UIImage imageWithContentsOfFile:filePath];
if (image == nil) {
OWSLogError(@"Could not read avatar image.");
return;
}
DatabaseStorageWrite(self.databaseStorage, ^(SDSAnyWriteTransaction *transaction) {
OWSUserProfile *currentUserProfile =
[OWSUserProfile getOrBuildUserProfileForAddress:userProfile.address transaction:transaction];
if (currentUserProfile.avatarFileName.length > 0) {
OWSLogVerbose(@"Aborting; avatar already present.");
return;
}
if (![NSObject isNullableObject:currentUserProfile.profileKey.keyData equalTo:profileKeyAtStart]
|| ![NSObject isNullableObject:currentUserProfile.avatarUrlPath equalTo:avatarUrlPathAtStart]) {
OWSLogVerbose(@"Aborting; profileKey or avatarUrlPath has changed.");
// If the profileKey or avatarUrlPath has changed,
// abort and kick off a new download if necessary.
if (currentUserProfile.avatarFileName == nil) {
[transaction
addAsyncCompletionOffMain:^{ [self downloadAvatarForUserProfile:currentUserProfile]; }];
}
}
[currentUserProfile updateWithAvatarFileName:filename
userProfileWriter:UserProfileWriter_AvatarDownload
transaction:transaction];
});
OWSAssertDebug(backgroundTask);
backgroundTask = nil;
});
});
}
- (void)updateProfileForAddress:(SignalServiceAddress *)addressParam
givenName:(nullable NSString *)givenName
familyName:(nullable NSString *)familyName
bio:(nullable NSString *)bio
bioEmoji:(nullable NSString *)bioEmoji
username:(nullable NSString *)username
isUuidCapable:(BOOL)isUuidCapable
avatarUrlPath:(nullable NSString *)avatarUrlPath
optionalAvatarFileUrl:(nullable NSURL *)optionalAvatarFileUrl
profileBadges:(nullable NSArray<OWSUserProfileBadgeInfo *> *)profileBadges
lastFetchDate:(NSDate *)lastFetchDate
userProfileWriter:(UserProfileWriter)userProfileWriter
transaction:(SDSAnyWriteTransaction *)writeTx
{
SignalServiceAddress *address = [OWSUserProfile resolveUserProfileAddress:addressParam];
OWSAssertDebug(address.isValid);
OWSAssertDebug(NSThread.isMainThread == NO);
OWSLogDebug(@"update profile for: %@ -> %@, givenName: %@, familyName: %@, bio: %@, bioEmoji: %@, avatar: %@, "
@"avatarFile: %@, userProfileWriter: %@",
addressParam,
address,
givenName,
familyName,
bio,
bioEmoji,
avatarUrlPath,
optionalAvatarFileUrl,
NSStringForUserProfileWriter(userProfileWriter));
OWSUserProfile *userProfile = [OWSUserProfile getOrBuildUserProfileForAddress:address transaction:writeTx];
if (!userProfile.profileKey) {
[userProfile updateWithUsername:username
isUuidCapable:isUuidCapable
lastFetchDate:lastFetchDate
userProfileWriter:userProfileWriter
transaction:writeTx];
} else if (optionalAvatarFileUrl.lastPathComponent) {
[userProfile updateWithGivenName:givenName
familyName:familyName
bio:bio
bioEmoji:bioEmoji
username:username
isUuidCapable:isUuidCapable
badges:profileBadges
avatarUrlPath:avatarUrlPath
avatarFileName:optionalAvatarFileUrl.lastPathComponent
lastFetchDate:lastFetchDate
userProfileWriter:userProfileWriter
transaction:writeTx
completion:nil];
} else {
[userProfile updateWithGivenName:givenName
familyName:familyName
bio:bio
bioEmoji:bioEmoji
username:username
isUuidCapable:isUuidCapable
badges:profileBadges
avatarUrlPath:avatarUrlPath
lastFetchDate:lastFetchDate
userProfileWriter:userProfileWriter
transaction:writeTx
completion:nil];
}
if (userProfile.profileKey && userProfile.avatarFileName.length > 0) {
NSString *path = [OWSUserProfile profileAvatarFilepathWithFilename:userProfile.avatarFileName];
if (![NSFileManager.defaultManager fileExistsAtPath:path]) {
OWSLogError(@"downloaded file is missing for profile: %@, userProfileWriter: %@",
userProfile.address,
NSStringForUserProfileWriter(userProfileWriter));
[userProfile updateWithAvatarFileName:nil userProfileWriter:userProfileWriter transaction:writeTx];
}
}
// Whenever we change avatarUrlPath, OWSUserProfile clears avatarFileName.
// So if avatarUrlPath is set and avatarFileName is not set, we should to
// download this avatar. downloadAvatarForUserProfile will de-bounce
// downloads.
if (userProfile.avatarUrlPath.length > 0 && userProfile.avatarFileName.length < 1) {
[self downloadAvatarForUserProfile:userProfile];
}
}
- (BOOL)isNullableDataEqual:(NSData *_Nullable)left toData:(NSData *_Nullable)right
{
if (left == nil && right == nil) {
return YES;
} else if (left == nil || right == nil) {
return YES;
} else {
return [left isEqual:right];
}
}
- (BOOL)isNullableStringEqual:(NSString *_Nullable)left toString:(NSString *_Nullable)right
{
if (left == nil && right == nil) {
return YES;
} else if (left == nil || right == nil) {
return YES;
} else {
return [left isEqualToString:right];
}
}
#pragma mark - Profile Encryption
- (BOOL)isProfileNameTooLong:(nullable NSString *)profileName
{
OWSAssertIsOnMainThread();
if (profileName.glyphCount > OWSUserProfile.maxNameLengthGlyphs) {
return YES;
}
NSData *nameData = [profileName dataUsingEncoding:NSUTF8StringEncoding];
return nameData.length > (NSUInteger)OWSUserProfile.maxNameLengthBytes;
}
#pragma mark - Avatar Disk Cache
- (nullable NSData *)loadProfileAvatarDataWithFilename:(NSString *)filename
{
OWSAssertDebug(filename.length > 0);
NSUInteger loadCount = [self.profileAvatarDataLoadCounter increment];
OWSLogVerbose(@"---- loading profile avatar data: %lu.", loadCount);
NSString *filePath = [OWSUserProfile profileAvatarFilepathWithFilename:filename];
NSData *_Nullable avatarData = [NSData dataWithContentsOfFile:filePath];
if (![avatarData ows_isValidImage]) {
OWSLogWarn(@"Failed to get valid avatar data");
return nil;
}
if (nil != avatarData) {
return avatarData;
}
OWSLogWarn(@"Could not load profile avatar data.");
return nil;
}
- (nullable UIImage *)loadProfileAvatarWithFilename:(NSString *)filename
{
if (filename.length == 0) {
return nil;
}
NSData *_Nullable data = [self loadProfileAvatarDataWithFilename:filename];
if (nil == data) {
return nil;
}
NSUInteger loadCount = [self.profileAvatarImageLoadCounter increment];
OWSLogVerbose(@"---- loading profile avatar image: %lu.", loadCount);
UIImage *_Nullable image = [UIImage imageWithData:data];
if (image) {
return image;
} else {
OWSLogWarn(@"Could not load profile avatar.");
return nil;
}
}
- (AnyPromise *)downloadAndDecryptProfileAvatarForProfileAddress:(SignalServiceAddress *)profileAddress
avatarUrlPath:(NSString *)avatarUrlPath
profileKey:(OWSAES256Key *)profileKey
{
return [OWSProfileManager avatarDownloadAndDecryptPromiseObjcWithProfileAddress:profileAddress
avatarUrlPath:avatarUrlPath
profileKey:profileKey];
}
- (nullable ModelReadCacheSizeLease *)leaseCacheSize:(NSInteger)size {
return [self.modelReadCaches.userProfileReadCache leaseCacheSize:size];
}
#pragma mark - Messaging History
- (void)didSendOrReceiveMessageFromAddress:(SignalServiceAddress *)addressParam
transaction:(SDSAnyWriteTransaction *)transaction
{
SignalServiceAddress *address = [OWSUserProfile resolveUserProfileAddress:addressParam];
OWSAssertDebug(address.isValid);
if (address.isLocalAddress) {
return;
}
OWSUserProfile *userProfile = [OWSUserProfile getOrBuildUserProfileForAddress:address transaction:transaction];
if (userProfile.lastMessagingDate != nil) {
// lastMessagingDate is coarse; we don't need to track
// every single message sent or received. It is sufficient
// to update it only when the value changes by more than
// an hour.
NSTimeInterval lastMessagingInterval = fabs(userProfile.lastMessagingDate.timeIntervalSinceNow);
const NSTimeInterval lastMessagingResolution = 1 * kHourInterval;
if (lastMessagingInterval < lastMessagingResolution) {
return;
}
}
[userProfile updateWithLastMessagingDate:[NSDate new]
userProfileWriter:UserProfileWriter_MetadataUpdate
transaction:transaction];
}
#pragma mark - Notifications
- (void)applicationDidBecomeActive:(NSNotification *)notification
{
OWSAssertIsOnMainThread();
// TODO: Sync if necessary.
[OWSProfileManager updateProfileOnServiceIfNecessary];
}
- (void)reachabilityChanged:(NSNotification *)notification
{
OWSAssertIsOnMainThread();
[OWSProfileManager updateProfileOnServiceIfNecessary];
}
- (void)blockListDidChange:(NSNotification *)notification {
OWSAssertIsOnMainThread();
AppReadinessRunNowOrWhenAppDidBecomeReadyAsync(^{ [self rotateLocalProfileKeyIfNecessary]; });
}
#ifdef DEBUG
+ (void)discardAllProfileKeysWithTransaction:(SDSAnyWriteTransaction *)transaction
{
NSArray<OWSUserProfile *> *userProfiles = [OWSUserProfile anyFetchAllWithTransaction:transaction];
for (OWSUserProfile *userProfile in userProfiles) {
if ([OWSUserProfile isLocalProfileAddress:userProfile.address]) {
continue;
}
if (userProfile.profileKey == nil) {
continue;
}
[userProfile discardProfileKeyWithUserProfileWriter:UserProfileWriter_Debugging transaction:transaction];
}
}
#endif
#pragma mark - Clean Up
+ (NSSet<NSString *> *)allProfileAvatarFilePathsWithTransaction:(SDSAnyReadTransaction *)transaction
{
return [OWSUserProfile allProfileAvatarFilePathsWithTransaction:transaction];
}
@end
NS_ASSUME_NONNULL_END