1314 lines
50 KiB
Objective-C
1314 lines
50 KiB
Objective-C
//
|
|
// Copyright 2017 Signal Messenger, LLC
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
//
|
|
|
|
#import "OWSProfileManager.h"
|
|
#import <SignalCoreKit/Cryptography.h>
|
|
#import <SignalCoreKit/NSData+OWS.h>
|
|
#import <SignalCoreKit/NSDate+OWS.h>
|
|
#import <SignalCoreKit/NSString+OWS.h>
|
|
#import <SignalServiceKit/AppContext.h>
|
|
#import <SignalServiceKit/AppReadiness.h>
|
|
#import <SignalServiceKit/MIMETypeUtil.h>
|
|
#import <SignalServiceKit/NSData+Image.h>
|
|
#import <SignalServiceKit/OWSFileSystem.h>
|
|
#import <SignalServiceKit/OWSProfileKeyMessage.h>
|
|
#import <SignalServiceKit/SignalServiceKit-Swift.h>
|
|
#import <SignalServiceKit/TSGroupThread.h>
|
|
#import <SignalServiceKit/TSThread.h>
|
|
#import <SignalServiceKit/UIImage+OWS.h>
|
|
|
|
NS_ASSUME_NONNULL_BEGIN
|
|
|
|
const NSUInteger kOWSProfileManager_MaxAvatarDiameterPixels = 1024;
|
|
NSString *const 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) id<RecipientHidingManager> recipientHidingManager;
|
|
|
|
@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
|
|
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;
|
|
_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
|
|
swiftValues:(OWSProfileManagerSwiftValues *)swiftValues
|
|
{
|
|
self = [super init];
|
|
|
|
if (!self) {
|
|
return self;
|
|
}
|
|
|
|
OWSAssertIsOnMainThread();
|
|
OWSAssertDebug(databaseStorage);
|
|
|
|
_whitelistedPhoneNumbersStore =
|
|
[[SDSKeyValueStore alloc] initWithCollection:@"kOWSProfileManager_UserWhitelistCollection"];
|
|
_whitelistedServiceIdsStore =
|
|
[[SDSKeyValueStore alloc] initWithCollection:@"kOWSProfileManager_UserUUIDWhitelistCollection"];
|
|
_whitelistedGroupsStore =
|
|
[[SDSKeyValueStore alloc] initWithCollection:@"kOWSProfileManager_GroupWhitelistCollection"];
|
|
_settingsStore = [[SDSKeyValueStore alloc] initWithCollection:@"kOWSProfileManager_SettingsStore"];
|
|
_metadataStore = [[SDSKeyValueStore alloc] initWithCollection:@"kOWSProfileManager_Metadata"];
|
|
_badgeStore = [[BadgeStore alloc] init];
|
|
_swiftValues = swiftValues;
|
|
|
|
OWSSingletonAssert();
|
|
|
|
AppReadinessRunNowOrWhenAppDidBecomeReadySync(^{
|
|
if (CurrentAppContext().isMainApp && [TSAccountManagerObjcBridge isRegisteredWithMaybeTransaction]) {
|
|
[self logLocalAvatarStatus];
|
|
}
|
|
});
|
|
|
|
AppReadinessRunNowOrWhenAppDidBecomeReadyAsync(^{
|
|
if ([TSAccountManagerObjcBridge isRegisteredPrimaryDeviceWithMaybeTransaction]) {
|
|
[self rotateLocalProfileKeyIfNecessary];
|
|
}
|
|
|
|
[self updateProfileOnServiceIfNecessaryWithAuthedAccount:AuthedAccount.implicit];
|
|
[OWSProfileManager updateStorageServiceIfNecessary];
|
|
});
|
|
|
|
[self observeNotifications];
|
|
|
|
return 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([TSAccountManagerObjcBridge isRegisteredWithMaybeTransaction]);
|
|
|
|
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 getUserProfileFor:OWSUserProfile.localProfileAddress
|
|
transaction:transaction];
|
|
}
|
|
file:__FILE__
|
|
function:__FUNCTION__
|
|
line:__LINE__];
|
|
[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 profileAvatarFilePathFor:localUserProfile.avatarFileName];
|
|
hasAvatarFileOnDisk = [OWSFileSystem fileOrFolderExistsAtPath:filePath];
|
|
}
|
|
|
|
if (SSKDebugFlags.internalLogging) {
|
|
OWSLogInfo(
|
|
@"Local user profile (%@). address: %@, avatarUrlPath: %@, avatarFileName: %@, hasAvatarFileOnDisk: %d",
|
|
label,
|
|
localUserProfile.internalAddress,
|
|
localUserProfile.avatarUrlPath,
|
|
localUserProfile.avatarFileName,
|
|
hasAvatarFileOnDisk);
|
|
} else {
|
|
OWSLogInfo(
|
|
@"Local user profile (%@). address: %@, avatarUrlPath: %d, avatarFileName: %d, hasAvatarFileOnDisk: %d",
|
|
label,
|
|
localUserProfile.internalAddress,
|
|
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 localUserProfileWithAuthedAccount:AuthedAccount.implicit];
|
|
}
|
|
|
|
#pragma mark - Local Profile
|
|
|
|
- (OWSUserProfile *)localUserProfileWithAuthedAccount:(AuthedAccount *)authedAccount
|
|
{
|
|
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
|
|
authedAccount:authedAccount
|
|
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 getUserProfileFor: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 doesLocalProfileExistWithTransaction: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.filteredGivenName;
|
|
}
|
|
|
|
- (nullable NSString *)localFamilyName
|
|
{
|
|
return self.localUserProfile.filteredFamilyName;
|
|
}
|
|
|
|
- (nullable NSString *)localFullName
|
|
{
|
|
return self.localUserProfile.filteredFullName;
|
|
}
|
|
|
|
- (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 NSArray<OWSUserProfileBadgeInfo *> *)localProfileBadgeInfo
|
|
{
|
|
return self.localUserProfile.badges;
|
|
}
|
|
|
|
- (OWSProfileSnapshot *)localProfileSnapshotWithShouldIncludeAvatar:(BOOL)shouldIncludeAvatar
|
|
{
|
|
return [self profileSnapshotForUserProfile:self.localUserProfile shouldIncludeAvatar:shouldIncludeAvatar];
|
|
}
|
|
|
|
- (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.filteredGivenName
|
|
familyName:userProfile.filteredFamilyName
|
|
fullName:userProfile.filteredFullName
|
|
bio:userProfile.bio
|
|
bioEmoji:userProfile.bioEmoji
|
|
avatarData:avatarData
|
|
profileBadgeInfo:userProfile.badges];
|
|
}
|
|
|
|
+ (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)reuploadLocalProfileWithAuthedAccount:(AuthedAccount *)authedAccount
|
|
{
|
|
[self reuploadLocalProfileWithSneakyTransactionWithAuthedAccount:authedAccount]
|
|
.done(^(id value) { OWSLogInfo(@"Done."); })
|
|
.catch(^(NSError *error) {
|
|
if (error.isNetworkFailureOrTimeout) {
|
|
OWSLogWarn(@"Error: %@", error);
|
|
} else {
|
|
OWSFailDebug(@"Error: %@", error);
|
|
}
|
|
});
|
|
}
|
|
|
|
#pragma mark - Profile Key Rotation
|
|
|
|
- (NSString *)groupKeyForGroupId:(NSData *)groupId
|
|
{
|
|
return [groupId hexadecimalString];
|
|
}
|
|
|
|
- (void)forceRotateLocalProfileKeyForGroupDepartureWithTransaction:(SDSAnyWriteTransaction *)transaction
|
|
{
|
|
[self forceRotateLocalProfileKeyForGroupDepartureObjcWithTx:transaction];
|
|
}
|
|
|
|
#pragma mark - Profile Whitelist
|
|
|
|
- (void)clearProfileWhitelist
|
|
{
|
|
OWSLogWarn(@"Clearing the profile whitelist.");
|
|
|
|
DatabaseStorageAsyncWrite(self.databaseStorage, ^(SDSAnyWriteTransaction *transaction) {
|
|
[self.whitelistedPhoneNumbersStore removeAllWithTransaction:transaction];
|
|
[self.whitelistedServiceIdsStore removeAllWithTransaction:transaction];
|
|
[self.whitelistedGroupsStore removeAllWithTransaction:transaction];
|
|
|
|
OWSAssertDebug(0 == [self.whitelistedPhoneNumbersStore numberOfKeysWithTransaction:transaction]);
|
|
OWSAssertDebug(0 == [self.whitelistedServiceIdsStore 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.whitelistedServiceIdsStore.collection,
|
|
(unsigned long)[self.whitelistedServiceIdsStore numberOfKeysWithTransaction:transaction]);
|
|
for (NSString *key in [self.whitelistedServiceIdsStore allKeysWithTransaction:transaction]) {
|
|
OWSLogError(@"\t profile whitelist user service id: %@", 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 clearProfileWithProfileKey:[OWSAES256Key generateRandomKey]
|
|
userProfileWriter:UserProfileWriter_Debugging
|
|
authedAccount:AuthedAccount.implicit
|
|
transaction:transaction
|
|
completion:nil];
|
|
});
|
|
[AccountAttributesUpdaterObjcBridge updateAccountAttributes].catch(
|
|
^(NSError *error) { OWSLogError(@"Error: %@.", error); });
|
|
}
|
|
|
|
- (void)setLocalProfileKey:(OWSAES256Key *)key
|
|
userProfileWriter:(UserProfileWriter)userProfileWriter
|
|
authedAccount:(AuthedAccount *)authedAccount
|
|
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-evaluate this method.
|
|
OWSFailDebug(@"Missing local profile when setting key.");
|
|
|
|
localUserProfile = [OWSUserProfile getOrBuildUserProfileForAddress:OWSUserProfile.localProfileAddress
|
|
authedAccount:authedAccount
|
|
transaction:transaction];
|
|
|
|
_localUserProfile = localUserProfile;
|
|
} else {
|
|
localUserProfile = _localUserProfile;
|
|
}
|
|
}
|
|
|
|
[localUserProfile updateWithProfileKey:key
|
|
userProfileWriter:userProfileWriter
|
|
authedAccount:authedAccount
|
|
transaction:transaction
|
|
completion:nil];
|
|
}
|
|
|
|
- (void)normalizeRecipientInProfileWhitelist:(SignalRecipient *)recipient tx:(SDSAnyWriteTransaction *)tx
|
|
{
|
|
[self swift_normalizeRecipientInProfileWhitelist:recipient tx:tx];
|
|
}
|
|
|
|
- (void)addUserToProfileWhitelist:(SignalServiceAddress *)address
|
|
userProfileWriter:(UserProfileWriter)userProfileWriter
|
|
transaction:(SDSAnyWriteTransaction *)transaction
|
|
{
|
|
OWSAssertDebug(address.isValid);
|
|
OWSAssertDebug(transaction);
|
|
|
|
[self addUsersToProfileWhitelist:@[ address ] 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];
|
|
[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 *)tx
|
|
{
|
|
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 transaction:tx] ||
|
|
[RecipientHidingManagerObjcBridge isHiddenAddress:address tx:tx]) {
|
|
continue;
|
|
}
|
|
|
|
if (![self isAddressInWhitelist:address tx:tx]) {
|
|
[notBlockedOrInWhitelist addObject:address];
|
|
}
|
|
}
|
|
|
|
return [notBlockedOrInWhitelist copy];
|
|
}
|
|
|
|
- (NSSet<SignalServiceAddress *> *)addressesInWhitelist:(NSArray<SignalServiceAddress *> *)addresses
|
|
transaction:(SDSAnyReadTransaction *)tx
|
|
{
|
|
OWSAssertDebug(addresses);
|
|
|
|
NSMutableSet<SignalServiceAddress *> *whitelistedAddresses = [NSMutableSet new];
|
|
|
|
for (SignalServiceAddress *address in addresses) {
|
|
if ([self isAddressInWhitelist:address tx:tx]) {
|
|
[whitelistedAddresses addObject:address];
|
|
}
|
|
}
|
|
|
|
return [whitelistedAddresses copy];
|
|
}
|
|
|
|
- (BOOL)isAddressInWhitelist:(SignalServiceAddress *)address tx:(SDSAnyReadTransaction *)tx
|
|
{
|
|
if (address.serviceIdUppercaseString) {
|
|
if ([self.whitelistedServiceIdsStore hasValueForKey:address.serviceIdUppercaseString transaction:tx]) {
|
|
return YES;
|
|
}
|
|
}
|
|
|
|
if (address.phoneNumber) {
|
|
if ([self.whitelistedPhoneNumbersStore hasValueForKey:address.phoneNumber transaction:tx]) {
|
|
return YES;
|
|
}
|
|
}
|
|
|
|
return NO;
|
|
}
|
|
|
|
- (void)removeConfirmedWhitelistedAddresses:(NSSet<SignalServiceAddress *> *)addressesToRemove
|
|
userProfileWriter:(UserProfileWriter)userProfileWriter
|
|
transaction:(SDSAnyWriteTransaction *)tx
|
|
{
|
|
if (addressesToRemove.count == 0) {
|
|
// Do nothing.
|
|
return;
|
|
}
|
|
|
|
for (SignalServiceAddress *address in addressesToRemove) {
|
|
// Historically we put both the ACI and phone number into their respective
|
|
// stores. We currently save only the best identifier, but we should still
|
|
// try and remove both to handle these historical cases.
|
|
if (address.serviceIdUppercaseString) {
|
|
[self.whitelistedServiceIdsStore removeValueForKey:address.serviceIdUppercaseString transaction:tx];
|
|
}
|
|
if (address.phoneNumber) {
|
|
[self.whitelistedPhoneNumbersStore removeValueForKey:address.phoneNumber transaction:tx];
|
|
}
|
|
|
|
TSThread *_Nullable thread = [TSContactThread getThreadWithContactAddress:address transaction:tx];
|
|
if (thread) {
|
|
[self.databaseStorage touchThread:thread shouldReindex:NO transaction:tx];
|
|
}
|
|
}
|
|
|
|
[tx addSyncCompletion:^{
|
|
// Mark the removed whitelisted addresses for update
|
|
if ([OWSUserProfile shouldUpdateStorageServiceForUserProfileWriter:userProfileWriter]) {
|
|
[self.storageServiceManagerObjc recordPendingUpdatesWithUpdatedAddresses:addressesToRemove.allObjects];
|
|
}
|
|
|
|
for (SignalServiceAddress *address in addressesToRemove) {
|
|
[[NSNotificationCenter defaultCenter]
|
|
postNotificationNameAsync:UserProfileNotifications.profileWhitelistDidChange
|
|
object:nil
|
|
userInfo:@{
|
|
UserProfileNotifications.profileAddressKey : address,
|
|
kNSNotificationKey_UserProfileWriter : @(userProfileWriter),
|
|
}];
|
|
}
|
|
}];
|
|
}
|
|
|
|
- (void)addConfirmedUnwhitelistedAddresses:(NSSet<SignalServiceAddress *> *)addressesToAdd
|
|
userProfileWriter:(UserProfileWriter)userProfileWriter
|
|
transaction:(SDSAnyWriteTransaction *)tx
|
|
{
|
|
if (addressesToAdd.count == 0) {
|
|
// Do nothing.
|
|
return;
|
|
}
|
|
|
|
for (SignalServiceAddress *address in addressesToAdd) {
|
|
ServiceIdObjC *serviceId = address.serviceIdObjC;
|
|
|
|
if ([serviceId isKindOfClass:[AciObjC class]]) {
|
|
[self.whitelistedServiceIdsStore setBool:YES key:serviceId.serviceIdUppercaseString transaction:tx];
|
|
} else if (address.phoneNumber) {
|
|
[self.whitelistedPhoneNumbersStore setBool:YES key:address.phoneNumber transaction:tx];
|
|
} else if ([serviceId isKindOfClass:[PniObjC class]]) {
|
|
[self.whitelistedServiceIdsStore setBool:YES key:serviceId.serviceIdUppercaseString transaction:tx];
|
|
}
|
|
|
|
TSThread *_Nullable thread = [TSContactThread getThreadWithContactAddress:address transaction:tx];
|
|
if (thread) {
|
|
[self.databaseStorage touchThread:thread shouldReindex:NO transaction:tx];
|
|
}
|
|
}
|
|
|
|
[tx addSyncCompletion:^{
|
|
// Mark the new whitelisted addresses for update
|
|
if ([OWSUserProfile shouldUpdateStorageServiceForUserProfileWriter:userProfileWriter]) {
|
|
[self.storageServiceManagerObjc recordPendingUpdatesWithUpdatedAddresses:addressesToAdd.allObjects];
|
|
}
|
|
|
|
for (SignalServiceAddress *address in addressesToAdd) {
|
|
[[NSNotificationCenter defaultCenter]
|
|
postNotificationNameAsync:UserProfileNotifications.profileWhitelistDidChange
|
|
object:nil
|
|
userInfo:@{
|
|
UserProfileNotifications.profileAddressKey : address,
|
|
kNSNotificationKey_UserProfileWriter : @(userProfileWriter),
|
|
}];
|
|
}
|
|
}];
|
|
}
|
|
|
|
- (BOOL)isUserInProfileWhitelist:(SignalServiceAddress *)address transaction:(SDSAnyReadTransaction *)tx
|
|
{
|
|
OWSAssertDebug(address.isValid);
|
|
|
|
if ([self.blockingManager isAddressBlocked:address transaction:tx]
|
|
|| ([RecipientHidingManagerObjcBridge isHiddenAddress:address tx:tx])) {
|
|
return NO;
|
|
}
|
|
|
|
return [self isAddressInWhitelist:address tx:tx];
|
|
}
|
|
|
|
- (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 ([OWSUserProfile shouldUpdateStorageServiceForUserProfileWriter:userProfileWriter]) {
|
|
[self recordPendingUpdatesForStorageServiceWithGroupId:groupId];
|
|
}
|
|
|
|
[[NSNotificationCenter defaultCenter]
|
|
postNotificationNameAsync:UserProfileNotifications.profileWhitelistDidChange
|
|
object:nil
|
|
userInfo:@{
|
|
UserProfileNotifications.profileGroupIdKey : 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 ([OWSUserProfile shouldUpdateStorageServiceForUserProfileWriter:userProfileWriter]) {
|
|
[self recordPendingUpdatesForStorageServiceWithGroupId:groupId];
|
|
}
|
|
|
|
[[NSNotificationCenter defaultCenter]
|
|
postNotificationNameAsync:UserProfileNotifications.profileWhitelistDidChange
|
|
object:nil
|
|
userInfo:@{
|
|
UserProfileNotifications.profileGroupIdKey : 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.storageServiceManagerObjc recordPendingUpdatesWithGroupModel:groupThread.groupModel];
|
|
}];
|
|
}
|
|
|
|
// 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 if ([thread isKindOfClass:[TSContactThread class]]) {
|
|
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 transaction:transaction]) {
|
|
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 if ([thread isKindOfClass:[TSContactThread class]]) {
|
|
TSContactThread *contactThread = (TSContactThread *)thread;
|
|
return [self isUserInProfileWhitelist:contactThread.contactAddress transaction:transaction];
|
|
} else {
|
|
return NO;
|
|
}
|
|
}
|
|
|
|
#pragma mark - Other User's Profiles
|
|
|
|
- (void)setProfileKeyData:(NSData *)profileKeyData
|
|
forAddress:(SignalServiceAddress *)address
|
|
userProfileWriter:(UserProfileWriter)userProfileWriter
|
|
authedAccount:(AuthedAccount *)authedAccount
|
|
transaction:(SDSAnyWriteTransaction *)transaction
|
|
{
|
|
[self setProfileKeyData:profileKeyData
|
|
forAddress:address
|
|
onlyFillInIfMissing:NO
|
|
userProfileWriter:userProfileWriter
|
|
authedAccount:authedAccount
|
|
transaction:transaction];
|
|
}
|
|
|
|
- (void)fillInProfileKeysForAllProfileKeys:(NSDictionary<SignalServiceAddress *, NSData *> *)allProfileKeys
|
|
authoritativeProfileKeys:(NSDictionary<SignalServiceAddress *, NSData *> *)authoritativeProfileKeys
|
|
userProfileWriter:(UserProfileWriter)userProfileWriter
|
|
authedAccount:(AuthedAccount *)authedAccount
|
|
{
|
|
DatabaseStorageAsyncWrite(self.databaseStorage, ^(SDSAnyWriteTransaction *transaction) {
|
|
for (SignalServiceAddress *address in authoritativeProfileKeys) {
|
|
NSData *_Nullable profileKeyData = allProfileKeys[address];
|
|
if (profileKeyData == nil) {
|
|
OWSFailDebug(@"Missing profileKeyData.");
|
|
continue;
|
|
}
|
|
[self setProfileKeyData:profileKeyData
|
|
forAddress:address
|
|
onlyFillInIfMissing:NO
|
|
userProfileWriter:userProfileWriter
|
|
authedAccount:authedAccount
|
|
transaction:transaction];
|
|
}
|
|
|
|
for (SignalServiceAddress *address in allProfileKeys) {
|
|
if (authoritativeProfileKeys[address] != nil) {
|
|
// We already stored this profile key as an
|
|
// authoritative one.
|
|
continue;
|
|
}
|
|
NSData *_Nullable profileKeyData = allProfileKeys[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
|
|
authedAccount:authedAccount
|
|
transaction:transaction];
|
|
}
|
|
});
|
|
}
|
|
|
|
- (void)setProfileGivenName:(nullable NSString *)givenName
|
|
familyName:(nullable NSString *)familyName
|
|
forAddress:(SignalServiceAddress *)addressParam
|
|
userProfileWriter:(UserProfileWriter)userProfileWriter
|
|
authedAccount:(AuthedAccount *)authedAccount
|
|
transaction:(SDSAnyWriteTransaction *)transaction
|
|
{
|
|
SignalServiceAddress *address = [OWSUserProfile internalAddressFor:addressParam];
|
|
if ([authedAccount isAddressForLocalUser:address]) {
|
|
address = [OWSUserProfile localProfileAddress];
|
|
}
|
|
OWSAssertDebug(address.isValid);
|
|
|
|
OWSUserProfile *userProfile = [OWSUserProfile getOrBuildUserProfileForAddress:address
|
|
authedAccount:authedAccount
|
|
transaction:transaction];
|
|
[userProfile updateWithGivenName:givenName
|
|
familyName:familyName
|
|
userProfileWriter:userProfileWriter
|
|
authedAccount:authedAccount
|
|
transaction:transaction
|
|
completion:nil];
|
|
}
|
|
|
|
- (void)setProfileGivenName:(nullable NSString *)givenName
|
|
familyName:(nullable NSString *)familyName
|
|
avatarUrlPath:(nullable NSString *)avatarUrlPath
|
|
forAddress:(SignalServiceAddress *)addressParam
|
|
userProfileWriter:(UserProfileWriter)userProfileWriter
|
|
authedAccount:(AuthedAccount *)authedAccount
|
|
transaction:(SDSAnyWriteTransaction *)transaction
|
|
{
|
|
SignalServiceAddress *address = [OWSUserProfile internalAddressFor:addressParam];
|
|
if ([authedAccount isAddressForLocalUser:address]) {
|
|
address = [OWSUserProfile localProfileAddress];
|
|
}
|
|
OWSAssertDebug(address.isValid);
|
|
|
|
OWSUserProfile *userProfile = [OWSUserProfile getOrBuildUserProfileForAddress:address
|
|
authedAccount:authedAccount
|
|
transaction:transaction];
|
|
[userProfile updateWithGivenName:givenName
|
|
familyName:familyName
|
|
avatarUrlPath:avatarUrlPath
|
|
userProfileWriter:userProfileWriter
|
|
authedAccount:authedAccount
|
|
transaction:transaction
|
|
completion:nil];
|
|
}
|
|
|
|
- (nullable NSData *)profileKeyDataForAddress:(SignalServiceAddress *)address
|
|
transaction:(SDSAnyReadTransaction *)transaction
|
|
{
|
|
return [self profileKeyForAddress:address transaction:transaction].keyData;
|
|
}
|
|
|
|
- (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.givenName;
|
|
}
|
|
|
|
- (nullable NSString *)givenNameForAddress:(SignalServiceAddress *)address
|
|
transaction:(SDSAnyReadTransaction *)transaction
|
|
{
|
|
OWSAssertDebug(address.isValid);
|
|
|
|
OWSUserProfile *_Nullable userProfile = [self getUserProfileForAddress:address transaction:transaction];
|
|
|
|
return userProfile.filteredGivenName;
|
|
}
|
|
|
|
- (nullable NSString *)unfilteredFamilyNameForAddress:(SignalServiceAddress *)address
|
|
transaction:(SDSAnyReadTransaction *)transaction
|
|
{
|
|
OWSAssertDebug(address.isValid);
|
|
|
|
OWSUserProfile *_Nullable userProfile = [self getUserProfileForAddress:address transaction:transaction];
|
|
|
|
return userProfile.familyName;
|
|
}
|
|
|
|
|
|
- (nullable NSString *)familyNameForAddress:(SignalServiceAddress *)address
|
|
transaction:(SDSAnyReadTransaction *)transaction
|
|
{
|
|
OWSAssertDebug(address.isValid);
|
|
|
|
OWSUserProfile *_Nullable userProfile = [self getUserProfileForAddress:address transaction:transaction];
|
|
|
|
return userProfile.filteredFamilyName;
|
|
}
|
|
|
|
- (nullable NSString *)fullNameForAddress:(SignalServiceAddress *)address
|
|
transaction:(SDSAnyReadTransaction *)transaction
|
|
{
|
|
OWSAssertDebug(address.isValid);
|
|
|
|
OWSUserProfile *_Nullable userProfile = [self getUserProfileForAddress:address transaction:transaction];
|
|
|
|
return userProfile.filteredFullName;
|
|
}
|
|
|
|
- (nullable UIImage *)profileAvatarForAddress:(SignalServiceAddress *)address
|
|
authedAccount:(AuthedAccount *)authedAccount
|
|
transaction:(SDSAnyReadTransaction *)transaction
|
|
{
|
|
OWSAssertDebug(address.isValid);
|
|
|
|
OWSUserProfile *_Nullable userProfile = [self getUserProfileForAddress:address transaction:transaction];
|
|
|
|
if (userProfile.avatarFileName.length > 0) {
|
|
return [self loadProfileAvatarWithFilename:userProfile.avatarFileName];
|
|
}
|
|
|
|
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 profileAvatarFilePathFor: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 nil;
|
|
}
|
|
|
|
return [self loadProfileAvatarDataWithFilename:userProfile.avatarFileName];
|
|
}
|
|
|
|
- (nullable NSString *)profileAvatarURLPathForAddress:(SignalServiceAddress *)address
|
|
authedAccount:(AuthedAccount *)authedAccount
|
|
transaction:(SDSAnyReadTransaction *)transaction
|
|
{
|
|
OWSAssertDebug(address.isValid);
|
|
|
|
OWSUserProfile *_Nullable userProfile = [self getUserProfileForAddress:address transaction:transaction];
|
|
|
|
return userProfile.avatarUrlPath;
|
|
}
|
|
|
|
- (NSArray<SignalServiceAddress *> *)allWhitelistedRegisteredAddressesWithTx:(SDSAnyReadTransaction *)tx
|
|
{
|
|
return [self objc_allWhitelistedRegisteredAddressesWithTx:tx];
|
|
}
|
|
|
|
- (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 internalAddressFor: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];
|
|
}
|
|
|
|
#pragma mark - Avatar Disk Cache
|
|
|
|
- (nullable NSData *)loadProfileAvatarDataWithFilename:(NSString *)filename
|
|
{
|
|
OWSAssertDebug(filename.length > 0);
|
|
|
|
NSString *filePath = [OWSUserProfile profileAvatarFilePathFor: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;
|
|
}
|
|
|
|
UIImage *_Nullable image = [UIImage imageWithData:data];
|
|
if (image) {
|
|
return image;
|
|
} else {
|
|
OWSLogWarn(@"Could not load profile avatar.");
|
|
return nil;
|
|
}
|
|
}
|
|
|
|
- (nullable ModelReadCacheSizeLease *)leaseCacheSize:(NSInteger)size
|
|
{
|
|
return [self.modelReadCaches.userProfileReadCache leaseCacheSize:size];
|
|
}
|
|
|
|
#pragma mark - Messaging History
|
|
|
|
- (void)didSendOrReceiveMessageFromAddress:(SignalServiceAddress *)addressParam
|
|
authedAccount:(AuthedAccount *)authedAccount
|
|
transaction:(SDSAnyWriteTransaction *)transaction
|
|
{
|
|
if (addressParam.isLocalAddress || [authedAccount isAddressForLocalUser:addressParam]) {
|
|
return;
|
|
}
|
|
|
|
SignalServiceAddress *address = [OWSUserProfile internalAddressFor:addressParam];
|
|
OWSAssertDebug(address.isValid);
|
|
|
|
OWSUserProfile *userProfile = [OWSUserProfile getOrBuildUserProfileForAddress:address
|
|
authedAccount:authedAccount
|
|
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
|
|
authedAccount:authedAccount
|
|
transaction:transaction];
|
|
}
|
|
|
|
- (void)rotateProfileKeyUponRecipientHideWithTx:(nonnull SDSAnyWriteTransaction *)tx
|
|
{
|
|
[self rotateProfileKeyUponRecipientHideObjCWithTx:tx];
|
|
}
|
|
|
|
#pragma mark - Notifications
|
|
|
|
- (void)applicationDidBecomeActive:(NSNotification *)notification
|
|
{
|
|
OWSAssertIsOnMainThread();
|
|
|
|
// TODO: Sync if necessary.
|
|
|
|
[self updateProfileOnServiceIfNecessaryWithAuthedAccount:AuthedAccount.implicit];
|
|
}
|
|
|
|
- (void)reachabilityChanged:(NSNotification *)notification
|
|
{
|
|
OWSAssertIsOnMainThread();
|
|
|
|
[self updateProfileOnServiceIfNecessaryWithAuthedAccount:AuthedAccount.implicit];
|
|
}
|
|
|
|
- (void)blockListDidChange:(NSNotification *)notification
|
|
{
|
|
OWSAssertIsOnMainThread();
|
|
|
|
AppReadinessRunNowOrWhenAppDidBecomeReadyAsync(^{ [self rotateLocalProfileKeyIfNecessary]; });
|
|
}
|
|
|
|
#pragma mark - Clean Up
|
|
|
|
+ (NSSet<NSString *> *)allProfileAvatarFilePathsWithTransaction:(SDSAnyReadTransaction *)transaction
|
|
{
|
|
return [OWSUserProfile allProfileAvatarFilePathsWithTx:transaction];
|
|
}
|
|
|
|
@end
|
|
|
|
NS_ASSUME_NONNULL_END
|