Fully switch to the new contact API

This should improve performance significantly as it avoids many unnecessary sync transactions. It also makes the code more readable.
This commit is contained in:
Niels Andriesse 2021-07-22 14:41:27 +10:00
parent 41fe33525c
commit 519ffa4405
14 changed files with 158 additions and 1044 deletions

View File

@ -445,9 +445,6 @@ static NSTimeInterval launchStartedAt;
[SNConfiguration performMainSetup];
// TODO: Once "app ready" logic is moved into AppSetup, move this line there.
[self.profileManager ensureLocalProfileCached];
// Note that this does much more than set a flag;
// it will also run all deferred blocks.
[AppReadiness setAppIsReady];
@ -533,7 +530,6 @@ static NSTimeInterval launchStartedAt;
// Start running the disappearing messages job in case the newly registered user
// enables this feature
[self.disappearingMessagesJob startIfNecessary];
[self.profileManager ensureLocalProfileCached];
// For non-legacy users, read receipts are on by default.
[self.readReceiptManager setAreReadReceiptsEnabled:YES];

View File

@ -17,12 +17,21 @@ extension Storage {
@objc(setContact:usingTransaction:)
public func setContact(_ contact: Contact, using transaction: Any) {
let oldContact = getContact(with: contact.sessionID)
let transaction = transaction as! YapDatabaseReadWriteTransaction
if contact.sessionID == getUserHexEncodedPublicKey() {
contact.isTrusted = true // Always trust ourselves
}
transaction.setObject(contact, forKey: contact.sessionID, inCollection: Storage.contactCollection)
transaction.addCompletionQueue(DispatchQueue.main) {
// Delete old profile picture if needed
if let oldProfilePictureFileName = oldContact?.profilePictureFileName {
let path = OWSUserProfile.profileAvatarFilepath(withFilename: oldProfilePictureFileName)
DispatchQueue.global(qos: .default).async {
OWSFileSystem.deleteFileIfExists(path)
}
}
// Post notification
let notificationCenter = NotificationCenter.default
notificationCenter.post(name: .contactUpdated, object: contact.sessionID)
if contact.sessionID == getUserHexEncodedPublicKey() {
@ -34,7 +43,7 @@ extension Storage {
}
}
public func getAllContacts() -> Set<Contact> {
@objc public func getAllContacts() -> Set<Contact> {
var result: Set<Contact> = []
Storage.read { transaction in
transaction.enumerateRows(inCollection: Storage.contactCollection) { _, object, _, _ in

View File

@ -40,16 +40,6 @@ extension Storage {
var result: Contact?
Storage.read { transaction in
result = Storage.shared.getContact(with: userPublicKey)
// HACK: Apparently it's still possible for the user's contact info to be missing
if result == nil, let profile = OWSUserProfile.fetch(uniqueId: kLocalProfileUniqueId, transaction: transaction),
let userPublicKey = Storage.shared.getUserPublicKey() {
let user = Contact(sessionID: userPublicKey)
user.name = profile.profileName
user.profilePictureURL = profile.avatarUrlPath
user.profilePictureFileName = profile.avatarFileName
user.profilePictureEncryptionKey = profile.profileKey
result = user
}
}
return result
}

View File

@ -181,24 +181,24 @@ extension MessageReceiver {
let storage = SNMessagingKitConfiguration.shared.storage
let transaction = transaction as! YapDatabaseReadWriteTransaction
// Profile
let userProfile = SNMessagingKitConfiguration.shared.storage.getUserProfile(using: transaction)
updateProfileIfNeeded(publicKey: userPublicKey, name: message.displayName, profilePictureURL: message.profilePictureURL,
profileKey: given(message.profileKey) { OWSAES256Key(data: $0)! }, sentTimestamp: message.sentTimestamp!, transaction: transaction)
transaction.addCompletionQueue(DispatchQueue.main) {
SSKEnvironment.shared.profileManager.downloadAvatar(for: userProfile)
let userProfile = storage.getUser() ?? Contact(sessionID: userPublicKey)
SSKEnvironment.shared.profileManager.downloadAvatar(forUserProfile: userProfile)
}
// Initial configuration sync
if !UserDefaults.standard[.hasSyncedInitialConfiguration] {
UserDefaults.standard[.hasSyncedInitialConfiguration] = true
NotificationCenter.default.post(name: .initialConfigurationMessageReceived, object: nil)
// Contacts
for contact in message.contacts {
let sessionID = contact.publicKey!
let userProfile = OWSUserProfile.getOrBuild(forRecipientId: sessionID, transaction: transaction)
userProfile.profileKey = given(contact.profileKey) { OWSAES256Key(data: $0)! }
userProfile.avatarUrlPath = contact.profilePictureURL
userProfile.profileName = contact.displayName
userProfile.save(with: transaction)
for contactInfo in message.contacts {
let sessionID = contactInfo.publicKey!
let contact = Contact(sessionID: sessionID)
contact.profilePictureEncryptionKey = given(contactInfo.profileKey) { OWSAES256Key(data: $0)! }
contact.profilePictureURL = contactInfo.profilePictureURL
contact.name = contactInfo.displayName
Storage.shared.setContact(contact, using: transaction)
let thread = TSContactThread.getOrCreateThread(withContactSessionID: sessionID, transaction: transaction)
thread.shouldBeVisible = true
thread.save(with: transaction)
@ -302,13 +302,10 @@ extension MessageReceiver {
private static func updateProfileIfNeeded(publicKey: String, name: String?, profilePictureURL: String?,
profileKey: OWSAES256Key?, sentTimestamp: UInt64, transaction: YapDatabaseReadWriteTransaction) {
let isCurrentUser = (publicKey == getUserHexEncodedPublicKey())
let profileManager = SSKEnvironment.shared.profileManager
let userDefaults = UserDefaults.standard
let owsProfile = isCurrentUser ? SNMessagingKitConfiguration.shared.storage.getUserProfile(using: transaction)
: OWSUserProfile.fetch(uniqueId: publicKey, transaction: transaction) // Old API
let contact = Storage.shared.getContact(with: publicKey) ?? Contact(sessionID: publicKey) // New API
// Name
if let name = name, (name != owsProfile?.profileName || contact.name != owsProfile?.profileName) {
if let name = name, name != contact.name {
let shouldUpdate: Bool
if isCurrentUser {
shouldUpdate = given(userDefaults[.lastDisplayNameUpdate]) { sentTimestamp > UInt64($0.timeIntervalSince1970 * 1000) } ?? true
@ -317,17 +314,14 @@ extension MessageReceiver {
}
if shouldUpdate {
if isCurrentUser {
owsProfile?.profileName = name
userDefaults[.lastDisplayNameUpdate] = Date(timeIntervalSince1970: TimeInterval(sentTimestamp / 1000))
} else {
profileManager.updateProfileForContact(withID: publicKey, displayName: name, with: transaction)
}
contact.name = name
}
}
// Profile picture & profile key
if let profileKey = profileKey, let profilePictureURL = profilePictureURL, profileKey.keyData.count == kAES256_KeyByteLength,
(profileKey != owsProfile?.profileKey || contact.profilePictureEncryptionKey != owsProfile?.profileKey) {
profileKey != contact.profilePictureEncryptionKey {
let shouldUpdate: Bool
if isCurrentUser {
shouldUpdate = given(userDefaults[.lastProfilePictureUpdate]) { sentTimestamp > UInt64($0.timeIntervalSince1970 * 1000) } ?? true
@ -336,20 +330,13 @@ extension MessageReceiver {
}
if shouldUpdate {
if isCurrentUser {
owsProfile?.avatarUrlPath = profilePictureURL
owsProfile?.profileKey = profileKey
userDefaults[.lastProfilePictureUpdate] = Date(timeIntervalSince1970: TimeInterval(sentTimestamp / 1000))
} else {
profileManager.setProfileKeyData(profileKey.keyData, forRecipientId: publicKey, avatarURL: profilePictureURL)
}
contact.profilePictureURL = profilePictureURL
contact.profilePictureEncryptionKey = profileKey
}
}
// Persist changes
if isCurrentUser { // In the case where it's someone else the profile will already be saved (updateProfileForContact and setProfileKeyData to that internally)
owsProfile?.save(with: transaction)
}
Storage.shared.setContact(contact, using: transaction)
}

View File

@ -17,7 +17,7 @@ public protocol SessionMessagingKitStorageProtocol {
func getUserKeyPair() -> ECKeyPair?
func getUserED25519KeyPair() -> Box.KeyPair?
func getUser() -> Contact?
func getUserProfile(using transaction: Any) -> OWSUserProfile
func getAllContacts() -> Set<Contact>
// MARK: - Closed Groups

View File

@ -6,83 +6,12 @@
NS_ASSUME_NONNULL_BEGIN
typedef void (^OWSUserProfileCompletion)(void);
@class OWSAES256Key;
extern NSString *const kNSNotificationName_LocalProfileDidChange;
extern NSString *const kNSNotificationName_OtherUsersProfileWillChange;
extern NSString *const kNSNotificationName_OtherUsersProfileDidChange;
extern NSString *const kNSNotificationKey_ProfileRecipientId;
extern NSString *const kNSNotificationKey_ProfileGroupId;
extern NSString *const kLocalProfileUniqueId;
// This class should be completely thread-safe.
//
// It is critical for coherency that all DB operations for this
// class should be done on OWSProfileManager's dbConnection.
@interface OWSUserProfile : TSYapDatabaseObject
@property (atomic, readonly) NSString *recipientId;
@property (atomic, nullable) OWSAES256Key *profileKey;
@property (atomic, nullable) NSString *profileName;
@property (atomic, nullable) NSString *avatarUrlPath;
// This filename is relative to OWSProfileManager.profileAvatarsDirPath.
@property (atomic, readonly, nullable) NSString *avatarFileName;
- (instancetype)init NS_UNAVAILABLE;
+ (OWSUserProfile *)getOrBuildUserProfileForRecipientId:(NSString *)recipientId
dbConnection:(YapDatabaseConnection *)dbConnection;
+ (OWSUserProfile *)getOrBuildUserProfileForRecipientId:(NSString *)recipientId transaction:(YapDatabaseReadWriteTransaction *)transaction;
+ (BOOL)localUserProfileExists:(YapDatabaseConnection *)dbConnection;
#pragma mark - Update With... Methods
- (void)updateWithProfileName:(nullable NSString *)profileName
avatarUrlPath:(nullable NSString *)avatarUrlPath
avatarFileName:(nullable NSString *)avatarFileName
transaction:(YapDatabaseReadWriteTransaction *)transaction
completion:(nullable OWSUserProfileCompletion)completion;
- (void)updateWithProfileName:(nullable NSString *)profileName
avatarUrlPath:(nullable NSString *)avatarUrlPath
avatarFileName:(nullable NSString *)avatarFileName
dbConnection:(YapDatabaseConnection *)dbConnection
completion:(nullable OWSUserProfileCompletion)completion;
- (void)updateWithProfileName:(nullable NSString *)profileName
avatarUrlPath:(nullable NSString *)avatarUrlPath
dbConnection:(YapDatabaseConnection *)dbConnection
completion:(nullable OWSUserProfileCompletion)completion;
- (void)updateWithAvatarUrlPath:(nullable NSString *)avatarUrlPath
avatarFileName:(nullable NSString *)avatarFileName
dbConnection:(YapDatabaseConnection *)dbConnection
completion:(nullable OWSUserProfileCompletion)completion;
- (void)updateWithAvatarFileName:(nullable NSString *)avatarFileName
dbConnection:(YapDatabaseConnection *)dbConnection
completion:(nullable OWSUserProfileCompletion)completion;
- (void)updateWithProfileKey:(OWSAES256Key *)profileKey
dbConnection:(YapDatabaseConnection *)dbConnection
completion:(nullable OWSUserProfileCompletion)completion;
- (void)updateWithProfileKey:(OWSAES256Key *)profileKey
transaction:(YapDatabaseReadWriteTransaction *)transaction
completion:(nullable OWSUserProfileCompletion)completion;
- (void)clearWithProfileKey:(OWSAES256Key *)profileKey
dbConnection:(YapDatabaseConnection *)dbConnection
completion:(nullable OWSUserProfileCompletion)completion;
#pragma mark - Profile Avatars Directory
+ (NSString *)profileAvatarFilepathWithFilename:(NSString *)filename;
+ (nullable NSError *)migrateToSharedData;
+ (NSString *)legacyProfileAvatarsDirPath;

View File

@ -19,456 +19,15 @@
NS_ASSUME_NONNULL_BEGIN
NSString *const kNSNotificationName_LocalProfileDidChange = @"kNSNotificationName_LocalProfileDidChange";
NSString *const kNSNotificationName_OtherUsersProfileWillChange = @"kNSNotificationName_OtherUsersProfileWillChange";
NSString *const kNSNotificationName_OtherUsersProfileDidChange = @"kNSNotificationName_OtherUsersProfileDidChange";
NSString *const kNSNotificationKey_ProfileRecipientId = @"kNSNotificationKey_ProfileRecipientId";
NSString *const kNSNotificationKey_ProfileGroupId = @"kNSNotificationKey_ProfileGroupId";
NSString *const kLocalProfileUniqueId = @"kLocalProfileUniqueId";
@interface OWSUserProfile ()
@property (atomic, nullable) NSString *avatarFileName;
@end
#pragma mark -
@implementation OWSUserProfile
@synthesize avatarUrlPath = _avatarUrlPath;
@synthesize avatarFileName = _avatarFileName;
@synthesize profileName = _profileName;
+ (NSString *)collection
{
// Legacy class name.
return @"UserProfile";
}
+ (OWSUserProfile *)getOrBuildUserProfileForRecipientId:(NSString *)recipientId
dbConnection:(YapDatabaseConnection *)dbConnection
{
__block OWSUserProfile *userProfile;
[dbConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
userProfile = [OWSUserProfile fetchObjectWithUniqueID:recipientId transaction:transaction];
}];
if (userProfile != nil) {
return userProfile;
}
[LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
userProfile = [OWSUserProfile getOrBuildUserProfileForRecipientId:recipientId transaction:transaction];
}];
return userProfile;
}
+ (OWSUserProfile *)getOrBuildUserProfileForRecipientId:(NSString *)recipientId transaction:(YapDatabaseReadWriteTransaction *)transaction
{
OWSUserProfile *userProfile = [OWSUserProfile fetchObjectWithUniqueID:recipientId transaction:transaction];
if (!userProfile) {
userProfile = [[OWSUserProfile alloc] initWithRecipientId:recipientId];
if ([recipientId isEqualToString:kLocalProfileUniqueId]) {
[userProfile updateWithProfileKey:[OWSAES256Key generateRandomKey]
transaction:transaction
completion:nil];
}
}
return userProfile;
}
+ (BOOL)localUserProfileExists:(YapDatabaseConnection *)dbConnection
{
__block BOOL result = NO;
[dbConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
result = [OWSUserProfile fetchObjectWithUniqueID:kLocalProfileUniqueId transaction:transaction] != nil;
}];
return result;
}
- (instancetype)initWithRecipientId:(NSString *)recipientId
{
self = [super initWithUniqueId:recipientId];
if (!self) {
return self;
}
_recipientId = recipientId;
return self;
}
#pragma mark - Dependencies
- (TSAccountManager *)tsAccountManager
{
return SSKEnvironment.shared.tsAccountManager;
}
#pragma mark -
- (NSString *)sessionID
{
if ([self.recipientId isEqual:kLocalProfileUniqueId]) {
return [SNGeneralUtilities getUserPublicKey];
} else {
return self.recipientId;
}
}
- (nullable NSString *)avatarUrlPath
{
@synchronized(self)
{
return _avatarUrlPath;
}
}
- (void)setAvatarUrlPath:(nullable NSString *)avatarUrlPath
{
@synchronized(self)
{
BOOL didChange = ![NSObject isNullableObject:_avatarUrlPath equalTo:avatarUrlPath];
_avatarUrlPath = avatarUrlPath;
if (didChange) {
// If the avatarURL changed, the avatarFileName can't be valid.
// Clear it.
self.avatarFileName = nil;
}
}
}
- (nullable NSString *)avatarFileName
{
@synchronized(self) {
return _avatarFileName;
}
}
- (void)setAvatarFileName:(nullable NSString *)avatarFileName
{
@synchronized(self) {
BOOL didChange = ![NSObject isNullableObject:_avatarFileName equalTo:avatarFileName];
if (!didChange) {
return;
}
if (_avatarFileName) {
NSString *oldAvatarFilePath = [OWSUserProfile profileAvatarFilepathWithFilename:_avatarFileName];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[OWSFileSystem deleteFileIfExists:oldAvatarFilePath];
});
}
_avatarFileName = avatarFileName;
}
}
#pragma mark - Update With... Methods
// Similar in spirit to [TSYapDatabaseObject applyChangeToSelfAndLatestCopy],
// but with significant differences.
//
// * We save if this entity is not in the database.
// * We skip redundant saves by diffing.
// * We kick off multi-device synchronization.
// * We fire "did change" notifications.
- (void)applyChanges:(void (^)(id))changeBlock
functionName:(const char *)functionName
dbConnection:(YapDatabaseConnection *)dbConnection
completion:(nullable OWSUserProfileCompletion)completion
{
[LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
[self applyChanges:changeBlock functionName:functionName transaction:transaction completion:completion];
}];
}
- (void)applyChanges:(void (^)(id))changeBlock
functionName:(const char *)functionName
transaction:(YapDatabaseReadWriteTransaction *)transaction
completion:(nullable OWSUserProfileCompletion)completion
{
// self might be the latest instance, so take a "before" snapshot
// before any changes have been made.
__block NSDictionary *beforeSnapshot = [self.dictionaryValue copy];
changeBlock(self);
BOOL didChange = YES;
NSString *collection = [[self class] collection];
OWSUserProfile *_Nullable latestInstance = [transaction objectForKey:self.uniqueId inCollection:collection];
if (latestInstance) {
// If self is NOT the latest instance, take a new "before" snapshot
// before updating.
if (self != latestInstance) {
beforeSnapshot = [latestInstance.dictionaryValue copy];
}
changeBlock(latestInstance);
NSDictionary *afterSnapshot = [latestInstance.dictionaryValue copy];
if ([beforeSnapshot isEqual:afterSnapshot]) {
didChange = NO;
} else {
[latestInstance saveWithTransaction:transaction];
}
} else {
[self saveWithTransaction:transaction];
}
if (completion) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), completion);
}
if (!didChange) {
return;
}
NSString *masterDeviceHexEncodedPublicKey = [NSUserDefaults.standardUserDefaults stringForKey:@"masterDeviceHexEncodedPublicKey"];
BOOL isLocalUserProfile = [self.recipientId isEqualToString:kLocalProfileUniqueId] || (masterDeviceHexEncodedPublicKey != nil && [self.recipientId isEqualToString:masterDeviceHexEncodedPublicKey]);
dispatch_async(dispatch_get_main_queue(), ^{
if (isLocalUserProfile) {
[[NSNotificationCenter defaultCenter] postNotificationNameAsync:kNSNotificationName_LocalProfileDidChange
object:nil
userInfo:nil];
} else {
[[NSNotificationCenter defaultCenter]
postNotificationNameAsync:kNSNotificationName_OtherUsersProfileWillChange
object:nil
userInfo:@{
kNSNotificationKey_ProfileRecipientId : self.recipientId,
}];
[[NSNotificationCenter defaultCenter]
postNotificationNameAsync:kNSNotificationName_OtherUsersProfileDidChange
object:nil
userInfo:@{
kNSNotificationKey_ProfileRecipientId : self.recipientId,
}];
}
});
}
- (void)updateWithProfileName:(nullable NSString *)profileName
avatarUrlPath:(nullable NSString *)avatarUrlPath
avatarFileName:(nullable NSString *)avatarFileName
transaction:(YapDatabaseReadWriteTransaction *)transaction
completion:(nullable OWSUserProfileCompletion)completion
{
[self applyChanges:^(OWSUserProfile *userProfile) {
[userProfile setProfileName:[profileName ows_stripped]];
// Always setAvatarUrlPath: before you setAvatarFileName: since
// setAvatarUrlPath: may clear the avatar filename.
[userProfile setAvatarUrlPath:avatarUrlPath];
[userProfile setAvatarFileName:avatarFileName];
}
functionName:__PRETTY_FUNCTION__
transaction:transaction
completion:completion];
SNContact *contact = [LKStorage.shared getContactWithSessionID:self.sessionID] ?: [[SNContact alloc] initWithSessionID:self.sessionID];
contact.name = [profileName ows_stripped];
contact.profilePictureURL = avatarUrlPath;
contact.profilePictureFileName = avatarFileName;
[LKStorage.shared setContact:contact usingTransaction:transaction];
}
- (void)updateWithProfileName:(nullable NSString *)profileName
avatarUrlPath:(nullable NSString *)avatarUrlPath
avatarFileName:(nullable NSString *)avatarFileName
dbConnection:(YapDatabaseConnection *)dbConnection
completion:(nullable OWSUserProfileCompletion)completion
{
[self applyChanges:^(OWSUserProfile *userProfile) {
[userProfile setProfileName:[profileName ows_stripped]];
// Always setAvatarUrlPath: before you setAvatarFileName: since
// setAvatarUrlPath: may clear the avatar filename.
[userProfile setAvatarUrlPath:avatarUrlPath];
[userProfile setAvatarFileName:avatarFileName];
}
functionName:__PRETTY_FUNCTION__
dbConnection:dbConnection
completion:completion];
SNContact *contact = [LKStorage.shared getContactWithSessionID:self.sessionID] ?: [[SNContact alloc] initWithSessionID:self.sessionID];
contact.name = [profileName ows_stripped];
contact.profilePictureURL = avatarUrlPath;
contact.profilePictureFileName = avatarFileName;
[LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
[LKStorage.shared setContact:contact usingTransaction:transaction];
}];
}
- (void)updateWithProfileName:(nullable NSString *)profileName
avatarUrlPath:(nullable NSString *)avatarUrlPath
dbConnection:(YapDatabaseConnection *)dbConnection
completion:(nullable OWSUserProfileCompletion)completion
{
[self applyChanges:^(OWSUserProfile *userProfile) {
[userProfile setProfileName:[profileName ows_stripped]];
[userProfile setAvatarUrlPath:avatarUrlPath];
}
functionName:__PRETTY_FUNCTION__
dbConnection:dbConnection
completion:completion];
SNContact *contact = [LKStorage.shared getContactWithSessionID:self.sessionID] ?: [[SNContact alloc] initWithSessionID:self.sessionID];
contact.name = [profileName ows_stripped];
contact.profilePictureURL = avatarUrlPath;
[LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
[LKStorage.shared setContact:contact usingTransaction:transaction];
}];
}
- (void)updateWithAvatarUrlPath:(nullable NSString *)avatarUrlPath
avatarFileName:(nullable NSString *)avatarFileName
dbConnection:(YapDatabaseConnection *)dbConnection
completion:(nullable OWSUserProfileCompletion)completion
{
[self applyChanges:^(OWSUserProfile *userProfile) {
// Always setAvatarUrlPath: before you setAvatarFileName: since
// setAvatarUrlPath: may clear the avatar filename.
[userProfile setAvatarUrlPath:avatarUrlPath];
[userProfile setAvatarFileName:avatarFileName];
}
functionName:__PRETTY_FUNCTION__
dbConnection:dbConnection
completion:completion];
SNContact *contact = [LKStorage.shared getContactWithSessionID:self.sessionID] ?: [[SNContact alloc] initWithSessionID:self.sessionID];
contact.profilePictureURL = avatarUrlPath;
contact.profilePictureFileName = avatarFileName;
[LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
[LKStorage.shared setContact:contact usingTransaction:transaction];
}];
}
- (void)updateWithAvatarFileName:(nullable NSString *)avatarFileName
dbConnection:(YapDatabaseConnection *)dbConnection
completion:(nullable OWSUserProfileCompletion)completion
{
[self applyChanges:^(OWSUserProfile *userProfile) {
[userProfile setAvatarFileName:avatarFileName];
}
functionName:__PRETTY_FUNCTION__
dbConnection:dbConnection
completion:completion];
SNContact *contact = [LKStorage.shared getContactWithSessionID:self.sessionID] ?: [[SNContact alloc] initWithSessionID:self.sessionID];
contact.profilePictureFileName = avatarFileName;
[LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
[LKStorage.shared setContact:contact usingTransaction:transaction];
}];
}
- (void)clearWithProfileKey:(OWSAES256Key *)profileKey
dbConnection:(YapDatabaseConnection *)dbConnection
completion:(nullable OWSUserProfileCompletion)completion
{
[self applyChanges:^(OWSUserProfile *userProfile) {
[userProfile setProfileKey:profileKey];
// [userProfile setProfileName:nil]; - Loki disabled until we include profile name inside the encrypted profile from the url
// Always setAvatarUrlPath: before you setAvatarFileName: since
// setAvatarUrlPath: may clear the avatar filename.
[userProfile setAvatarUrlPath:nil];
[userProfile setAvatarFileName:nil];
}
functionName:__PRETTY_FUNCTION__
dbConnection:dbConnection
completion:completion];
}
- (void)updateWithProfileKey:(OWSAES256Key *)profileKey
dbConnection:(YapDatabaseConnection *)dbConnection
completion:(nullable OWSUserProfileCompletion)completion
{
[LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
[self updateWithProfileKey:profileKey transaction:transaction completion:completion];
}];
}
- (void)updateWithProfileKey:(OWSAES256Key *)profileKey
transaction:(YapDatabaseReadWriteTransaction *)transaction
completion:(nullable OWSUserProfileCompletion)completion
{
[self applyChanges:^(OWSUserProfile *userProfile) {
[userProfile setProfileKey:profileKey];
}
functionName:__PRETTY_FUNCTION__
transaction:transaction
completion:completion];
SNContact *contact = [LKStorage.shared getContactWithSessionID:self.sessionID] ?: [[SNContact alloc] initWithSessionID:self.sessionID];
contact.profilePictureEncryptionKey = profileKey;
[LKStorage.shared setContact:contact usingTransaction:transaction];
}
#pragma mark - Database Connection Accessors
- (YapDatabaseConnection *)dbReadConnection
{
return TSYapDatabaseObject.dbReadConnection;
}
+ (YapDatabaseConnection *)dbReadConnection
{
return TSYapDatabaseObject.dbReadConnection;
}
- (YapDatabaseConnection *)dbReadWriteConnection
{
return TSYapDatabaseObject.dbReadWriteConnection;
}
+ (YapDatabaseConnection *)dbReadWriteConnection
{
return TSYapDatabaseObject.dbReadWriteConnection;
}
// This should only be used in verbose, developer-only logs.
- (NSString *)debugDescription
{
return [NSString stringWithFormat:@"%@ %p %@ %lu %@ %@ %@",
@"OWSUserProfile",
self,
self.recipientId,
(unsigned long)self.profileKey.keyData.length,
self.profileName,
self.avatarUrlPath,
self.avatarFileName];
}
- (nullable NSString *)profileName
{
@synchronized(self)
{
return _profileName.filterStringForDisplay;
}
}
- (void)setProfileName:(nullable NSString *)profileName
{
@synchronized(self)
{
_profileName = profileName.filterStringForDisplay;
}
}
#pragma mark - Profile Avatars Directory
+ (NSString *)profileAvatarFilepathWithFilename:(NSString *)filename
{
if (filename.length <= 0) { return @""; };
@ -504,8 +63,6 @@ NSString *const kLocalProfileUniqueId = @"kLocalProfileUniqueId";
return profileAvatarsDirPath;
}
// TODO: We may want to clean up this directory in the "orphan cleanup" logic.
+ (void)resetProfileStorage
{
NSError *error;
@ -517,22 +74,14 @@ NSString *const kLocalProfileUniqueId = @"kLocalProfileUniqueId";
NSString *profileAvatarsDirPath = self.profileAvatarsDirPath;
NSMutableSet<NSString *> *profileAvatarFilePaths = [NSMutableSet new];
[OWSPrimaryStorage.sharedManager.dbReadConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
[OWSUserProfile
enumerateCollectionObjectsWithTransaction:transaction
usingBlock:^(id object, BOOL *stop) {
if (![object isKindOfClass:[OWSUserProfile class]]) {
return;
}
OWSUserProfile *userProfile = object;
if (!userProfile.avatarFileName) {
return;
}
NSString *filePath = [profileAvatarsDirPath
stringByAppendingPathComponent:userProfile.avatarFileName];
[profileAvatarFilePaths addObject:filePath];
}];
}];
NSSet<SNContact *> *allContacts = [LKStorage.shared getAllContacts];
for (SNContact *contact in allContacts) {
if (contact.profilePictureFileName == nil) { continue; }
NSString *filePath = [profileAvatarsDirPath stringByAppendingPathComponent:contact.profilePictureFileName];
[profileAvatarFilePaths addObject:filePath];
}
return [profileAvatarFilePaths copy];
}

View File

@ -5,7 +5,7 @@
@class OWSAES256Key;
@class TSThread;
@class YapDatabaseReadWriteTransaction;
@class OWSUserProfile;
@class SNContact;
NS_ASSUME_NONNULL_BEGIN
@ -13,7 +13,6 @@ NS_ASSUME_NONNULL_BEGIN
#pragma mark - Local Profile
- (void)ensureLocalProfileCached;
- (void)updateServiceWithProfileName:(nullable NSString *)localProfileName avatarURL:(nullable NSString *)avatarURL;
#pragma mark - Other User's Profiles
@ -21,12 +20,10 @@ NS_ASSUME_NONNULL_BEGIN
- (nullable NSData *)profileKeyDataForRecipientId:(NSString *)recipientId;
- (void)setProfileKeyData:(NSData *)profileKeyData forRecipientId:(NSString *)recipientId;
- (void)setProfileKeyData:(NSData *)profileKeyData forRecipientId:(NSString *)recipientId avatarURL:(nullable NSString *)avatarURL;
- (void)updateProfileForContactWithID:(NSString *)contactID displayName:(NSString *)displayName with:(YapDatabaseReadWriteTransaction *)transaction;
- (void)ensureProfileCachedForContactWithID:(NSString *)contactID with:(YapDatabaseReadWriteTransaction *)transaction;
#pragma mark - Other
- (void)downloadAvatarForUserProfile:(OWSUserProfile *)userProfile;
- (void)downloadAvatarForUserProfile:(SNContact *)userProfile;
@end

View File

@ -177,7 +177,7 @@ public class FullTextSearchFinder: NSObject {
}
private static let recipientIndexer: SearchIndexer<String> = SearchIndexer { (recipientId: String, transaction: YapDatabaseReadTransaction) in
let displayName = OWSUserProfile.fetch(uniqueId: recipientId, transaction: transaction)?.profileName ?? recipientId
let displayName = Storage.shared.getContact(with: recipientId)?.displayName(for: Contact.Context.regular) ?? recipientId
return "\(recipientId) \(displayName)"
}

View File

@ -103,9 +103,6 @@ final class ShareVC : UINavigationController, ShareViewDelegate, AppModeManagerD
Logger.debug("")
// TODO: Once "app ready" logic is moved into AppSetup, move this line there.
OWSProfileManager.shared().ensureLocalProfileCached()
// Note that this does much more than set a flag;
// it will also run all deferred blocks.
AppReadiness.setAppIsReady()
@ -123,8 +120,6 @@ final class ShareVC : UINavigationController, ShareViewDelegate, AppModeManagerD
// We don't need to use OWSMessageReceiver in the SAE.
// We don't need to use OWSBatchMessageProcessor in the SAE.
OWSProfileManager.shared().ensureLocalProfileCached()
// We don't need to use OWSOrphanDataCleaner in the SAE.
// We don't need to fetch the local profile in the SAE

View File

@ -5,8 +5,4 @@ extension Storage : SessionMessagingKitStorageProtocol, SessionSnodeKitStoragePr
let transaction = transaction as! YapDatabaseReadWriteTransaction
OWSPrimaryStorage.shared().updateMessageIDCollectionByPruningMessagesWithIDs(messageIDs, in: transaction)
}
public func getUserProfile(using transaction: Any) -> OWSUserProfile {
return OWSProfileManager.shared().getLocalUserProfile(with: transaction as! YapDatabaseReadWriteTransaction)
}
}

View File

@ -31,18 +31,18 @@ extension ConfigurationMessage {
default: break
}
}
OWSUserProfile.enumerateCollectionObjects(with: transaction) { object, stop in
guard let profile = object as? OWSUserProfile, let displayName = profile.profileName else { return }
let publicKey = profile.recipientId
var truncatedContacts = storage.getAllContacts()
if truncatedContacts.count > 200 { truncatedContacts = Set(Array(truncatedContacts)[0..<200]) }
truncatedContacts.forEach { contact in
let publicKey = contact.sessionID
let threadID = TSContactThread.threadID(fromContactSessionID: publicKey)
guard let thread = TSContactThread.fetch(uniqueId: threadID, transaction: transaction), thread.shouldBeVisible
&& !SSKEnvironment.shared.blockingManager.isRecipientIdBlocked(publicKey) else { return }
let profilePictureURL = profile.avatarUrlPath
let profileKey = profile.profileKey?.keyData
let contact = ConfigurationMessage.Contact(publicKey: publicKey, displayName: displayName,
let profilePictureURL = contact.profilePictureURL
let profileKey = contact.profilePictureEncryptionKey?.keyData
let contact = ConfigurationMessage.Contact(publicKey: publicKey, displayName: contact.name ?? publicKey,
profilePictureURL: profilePictureURL, profileKey: profileKey)
contacts.insert(contact)
guard contactCount < 200 else { stop.pointee = true; return }
contactCount += 1
}
}

View File

@ -35,8 +35,6 @@ extern const NSUInteger kOWSProfileManager_MaxAvatarDiameter;
// hasLocalProfile is true if there is a local profile with a name or avatar.
- (BOOL)hasLocalProfile;
- (OWSUserProfile *)getLocalUserProfileWithTransaction:(YapDatabaseReadWriteTransaction *)transaction;
// This method is used to update the "local profile" state on the client
// and the service. Client state is only updated if service state is
// successfully updated.
@ -61,11 +59,9 @@ extern const NSUInteger kOWSProfileManager_MaxAvatarDiameter;
profileNameEncrypted:(nullable NSData *)profileNameEncrypted
avatarUrlPath:(nullable NSString *)avatarUrlPath;
- (void)ensureProfileCachedForContactWithID:(NSString *)contactID with:(YapDatabaseReadWriteTransaction *)transaction;
#pragma mark - Other
- (void)downloadAvatarForUserProfile:(OWSUserProfile *)userProfile;
- (void)downloadAvatarForUserProfile:(SNContact *)contact;
@end

View File

@ -45,9 +45,6 @@ typedef void (^ProfileManagerFailureBlock)(NSError *error);
@property (nonatomic, readonly) YapDatabaseConnection *dbConnection;
// This property can be accessed on any thread, while synchronized on self.
@property (atomic, readonly) OWSUserProfile *localUserProfile;
// This property can be accessed on any thread, while synchronized on self.
@property (atomic, readonly) NSCache<NSString *, UIImage *> *profileAvatarImageCache;
@ -62,8 +59,6 @@ typedef void (^ProfileManagerFailureBlock)(NSError *error);
// Writes should happen off the main thread, wherever possible.
@implementation OWSProfileManager
@synthesize localUserProfile = _localUserProfile;
+ (instancetype)sharedManager
{
return SSKEnvironment.shared.profileManager;
@ -87,12 +82,6 @@ typedef void (^ProfileManagerFailureBlock)(NSError *error);
OWSSingletonAssert();
[AppReadiness runNowOrWhenAppDidBecomeReady:^{
[self rotateLocalProfileKeyIfNecessary];
}];
[self observeNotifications];
return self;
}
@ -101,14 +90,6 @@ typedef void (^ProfileManagerFailureBlock)(NSError *error);
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
- (void)observeNotifications
{
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(blockListDidChange:)
name:kNSNotificationName_BlockListDidChange
object:nil];
}
#pragma mark - Dependencies
- (TSAccountManager *)tsAccountManager
@ -126,78 +107,6 @@ typedef void (^ProfileManagerFailureBlock)(NSError *error);
return SSKEnvironment.shared.blockingManager;
}
#pragma mark - User Profile Accessor
- (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
{
if (_localUserProfile) { return _localUserProfile; }
__block OWSUserProfile *userProfile;
[LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
userProfile = [self getLocalUserProfileWithTransaction:transaction];
}];
return userProfile;
}
- (OWSUserProfile *)getLocalUserProfileWithTransaction:(YapDatabaseReadWriteTransaction *)transaction
{
@synchronized(self)
{
if (!_localUserProfile) {
_localUserProfile = [OWSUserProfile getOrBuildUserProfileForRecipientId:kLocalProfileUniqueId transaction:transaction];
}
}
OWSAssertDebug(_localUserProfile.profileKey);
return _localUserProfile;
}
- (BOOL)localProfileExists
{
return [OWSUserProfile localUserProfileExists:self.dbConnection];
}
- (OWSAES256Key *)localProfileKey
{
OWSAssertDebug(self.localUserProfile.profileKey.keyData.length == kAES256_KeyByteLength);
return self.localUserProfile.profileKey;
}
- (BOOL)hasLocalProfile
{
return (self.localProfileName.length > 0 || self.localProfileAvatarImage != nil);
}
- (nullable NSString *)localProfileName
{
return self.localUserProfile.profileName;
}
- (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 loadProfileDataWithFilename:filename];
}
- (void)updateLocalProfileName:(nullable NSString *)profileName
avatarImage:(nullable UIImage *)avatarImage
success:(void (^)(void))successBlockParameter
@ -232,27 +141,29 @@ typedef void (^ProfileManagerFailureBlock)(NSError *error);
[self updateServiceWithProfileName:profileName
avatarUrl:avatarUrlPath
success:^{
OWSUserProfile *userProfile = self.localUserProfile;
SNContact *userProfile = [LKStorage.shared getUser];
OWSAssertDebug(userProfile);
[userProfile updateWithProfileName:profileName
avatarUrlPath:avatarUrlPath
avatarFileName:avatarFileName
dbConnection:self.dbConnection
completion:^{
if (avatarFileName) {
[self updateProfileAvatarCache:avatarImage filename:avatarFileName];
}
userProfile.name = profileName;
userProfile.profilePictureURL = avatarUrlPath;
userProfile.profilePictureFileName = avatarFileName;
successBlock();
}];
[LKStorage writeWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
[LKStorage.shared setContact:userProfile usingTransaction:transaction];
} completion:^{
if (avatarFileName != nil) {
[self updateProfileAvatarCache:avatarImage filename:avatarFileName];
}
successBlock();
}];
}
failure:^(NSError *error) {
failureBlock(error);
}];
};
OWSUserProfile *userProfile = self.localUserProfile;
SNContact *userProfile = [LKStorage.shared getUser];
OWSAssertDebug(userProfile);
if (avatarImage) {
@ -277,7 +188,7 @@ typedef void (^ProfileManagerFailureBlock)(NSError *error);
failure:^(NSError *error) {
failureBlock(error);
}];
} else if (userProfile.avatarUrlPath) {
} else if (userProfile.profilePictureURL) {
OWSLogVerbose(@"Updating local profile on service with cleared avatar.");
[self uploadAvatarToService:nil
success:^(NSString *_Nullable avatarUrlPath) {
@ -292,11 +203,6 @@ typedef void (^ProfileManagerFailureBlock)(NSError *error);
}
}
- (nullable NSString *)profilePictureURL
{
return self.localUserProfile.avatarUrlPath;
}
- (void)writeAvatarToDisk:(UIImage *)avatar
success:(void (^)(NSData *data, NSString *fileName))successBlock
failure:(ProfileManagerFailureBlock)failureBlock {
@ -367,8 +273,13 @@ typedef void (^ProfileManagerFailureBlock)(NSError *error);
[promise.thenOn(dispatch_get_main_queue(), ^(NSString *fileID) {
NSString *downloadURL = [NSString stringWithFormat:@"%@/files/%@", SNFileServerAPIV2.server, fileID];
[NSUserDefaults.standardUserDefaults setObject:[NSDate new] forKey:@"lastProfilePictureUpload"];
[self.localUserProfile updateWithProfileKey:newProfileKey dbConnection:self.dbConnection completion:^{
successBlock(downloadURL);
SNContact *user = [LKStorage.shared getUser];
user.profilePictureEncryptionKey = newProfileKey;
[LKStorage writeWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
[LKStorage.shared setContact:user usingTransaction:transaction];
} completion:^{
successBlock(downloadURL);
}];
})
.catchOn(dispatch_get_main_queue(), ^(id result) {
@ -376,7 +287,11 @@ typedef void (^ProfileManagerFailureBlock)(NSError *error);
// to be invoked with the fulfilled promise's value as the error. The below
// is a quick and dirty workaround.
if ([result isKindOfClass:NSString.class]) {
[self.localUserProfile updateWithProfileKey:newProfileKey dbConnection:self.dbConnection completion:^{
SNContact *user = [LKStorage.shared getUser];
user.profilePictureEncryptionKey = newProfileKey;
[LKStorage writeWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
[LKStorage.shared setContact:user usingTransaction:transaction];
} completion:^{
successBlock(result);
}];
} else {
@ -385,7 +300,13 @@ typedef void (^ProfileManagerFailureBlock)(NSError *error);
}) retainUntilComplete];
} else {
// Update our profile key and set the url to nil if avatar data is nil
[self.localUserProfile updateWithProfileKey:newProfileKey dbConnection:self.dbConnection completion:^{
SNContact *user = [LKStorage.shared getUser];
user.profilePictureEncryptionKey = newProfileKey;
user.profilePictureURL = nil;
user.profilePictureFileName = nil;
[LKStorage writeWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
[LKStorage.shared setContact:user usingTransaction:transaction];
} completion:^{
successBlock(nil);
}];
}
@ -438,217 +359,18 @@ typedef void (^ProfileManagerFailureBlock)(NSError *error);
return [groupId copy];
}
- (void)rotateLocalProfileKeyIfNecessary {
[self rotateLocalProfileKeyIfNecessaryWithSuccess:^{ } failure:^(NSError *error) { }];
}
- (void)rotateLocalProfileKeyIfNecessaryWithSuccess:(dispatch_block_t)success
failure:(ProfileManagerFailureBlock)failure {
OWSAssertDebug(AppReadiness.isAppReady);
if (!self.tsAccountManager.isRegistered) {
success();
return;
}
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSMutableSet<NSString *> *whitelistedRecipientIds = [NSMutableSet new];
NSMutableSet<NSData *> *whitelistedGroupIds = [NSMutableSet new];
[self.dbConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
[whitelistedRecipientIds
addObjectsFromArray:[transaction allKeysInCollection:kOWSProfileManager_UserWhitelistCollection]];
NSArray<NSString *> *whitelistedGroupKeys =
[transaction allKeysInCollection:kOWSProfileManager_GroupWhitelistCollection];
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 `group.recipientIds` to the `whitelistedRecipientIds`.
//
// 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 redundnatly
// 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.
//
// However, after we did. The next time we check if we should rotate our profile
// key, adding all `group.recipientIds` to `whitelistedRecipientIds` here, would
// include Alice, and we'd rotate our profile key every time this method is called.
}
}];
NSString *_Nullable localNumber = [TSAccountManager localNumber];
if (localNumber) {
[whitelistedRecipientIds removeObject:localNumber];
} else {
OWSFailDebug(@"Missing localNumber");
}
NSSet<NSString *> *blockedRecipientIds = [NSSet setWithArray:self.blockingManager.blockedPhoneNumbers];
NSSet<NSData *> *blockedGroupIds = [NSSet setWithArray:self.blockingManager.blockedGroupIds];
// Find the users and groups which are both a) blocked b) may have our current profile key.
NSMutableSet<NSString *> *intersectingRecipientIds = [blockedRecipientIds mutableCopy];
[intersectingRecipientIds intersectSet:whitelistedRecipientIds];
NSMutableSet<NSData *> *intersectingGroupIds = [blockedGroupIds mutableCopy];
[intersectingGroupIds intersectSet:whitelistedGroupIds];
BOOL isProfileKeySharedWithBlocked = (intersectingRecipientIds.count > 0 || intersectingGroupIds.count > 0);
if (!isProfileKeySharedWithBlocked) {
// No need to rotate the profile key.
return success();
}
[self rotateProfileKeyWithIntersectingRecipientIds:intersectingRecipientIds
intersectingGroupIds:intersectingGroupIds
success:success
failure:failure];
});
}
- (void)rotateProfileKeyWithIntersectingRecipientIds:(NSSet<NSString *> *)intersectingRecipientIds
intersectingGroupIds:(NSSet<NSData *> *)intersectingGroupIds
success:(dispatch_block_t)success
failure:(ProfileManagerFailureBlock)failure {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// Rotate the profile key
OWSLogInfo(@"Rotating the profile key.");
// Make copies of the current local profile state.
OWSUserProfile *localUserProfile = self.localUserProfile;
NSString *_Nullable oldProfileName = localUserProfile.profileName;
NSString *_Nullable oldAvatarFileName = localUserProfile.avatarFileName;
NSData *_Nullable oldAvatarData = [self profileAvatarDataForRecipientId:self.tsAccountManager.localNumber];
// Rotate the stored profile key.
AnyPromise *promise = [AnyPromise promiseWithResolverBlock:^(PMKResolver resolve) {
[self.localUserProfile updateWithProfileKey:[OWSAES256Key generateRandomKey]
dbConnection:self.dbConnection
completion:^{
// The value doesn't matter, we just need any non-NSError value.
resolve(@(1));
}];
}];
// Try to re-upload our profile name, if any.
//
// This may fail.
promise = promise.then(^(id value) {
if (oldProfileName.length < 1) {
return [AnyPromise promiseWithValue:@(1)];
}
return [AnyPromise promiseWithResolverBlock:^(PMKResolver resolve) {
[self updateServiceWithProfileName:oldProfileName
avatarUrl:localUserProfile.avatarUrlPath
success:^{
OWSLogInfo(@"Update to profile name succeeded.");
// The value doesn't matter, we just need any non-NSError value.
resolve(@(1));
}
failure:^(NSError *error) {
resolve(error);
}];
}];
});
// Try to re-upload our profile avatar, if any.
//
// This may fail.
promise = promise.then(^(id value) {
if (oldAvatarData.length < 1 || oldAvatarFileName.length < 1) {
return [AnyPromise promiseWithValue:@(1)];
}
return [AnyPromise promiseWithResolverBlock:^(PMKResolver resolve) {
[self uploadAvatarToService:oldAvatarData
success:^(NSString *_Nullable avatarUrlPath) {
OWSLogInfo(@"Update to profile avatar after profile key rotation succeeded.");
// The profile manager deletes the underlying file when updating a profile URL
// So we need to copy the underlying file to a new location.
NSString *oldPath = [OWSUserProfile profileAvatarFilepathWithFilename:oldAvatarFileName];
NSString *newAvatarFilename = [self generateAvatarFilename];
NSString *newPath = [OWSUserProfile profileAvatarFilepathWithFilename:newAvatarFilename];
NSError *error;
[NSFileManager.defaultManager copyItemAtPath:oldPath toPath:newPath error:&error];
OWSAssertDebug(!error);
[self.localUserProfile updateWithAvatarUrlPath:avatarUrlPath
avatarFileName:newAvatarFilename
dbConnection:self.dbConnection
completion:^{
// The value doesn't matter, we just need any
// non-NSError value.
resolve(@(1));
}];
}
failure:^(NSError *error) {
OWSLogInfo(@"Update to profile avatar after profile key rotation failed.");
resolve(error);
}];
}];
});
// Try to re-upload our profile avatar, if any.
//
// This may fail.
promise = promise.then(^(id value) {
// Remove blocked users and groups from profile whitelist.
//
// This will always succeed.
[LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
[transaction removeObjectsForKeys:intersectingRecipientIds.allObjects
inCollection:kOWSProfileManager_UserWhitelistCollection];
for (NSData *groupId in intersectingGroupIds) {
NSString *groupIdKey = [self groupKeyForGroupId:groupId];
[transaction removeObjectForKey:groupIdKey
inCollection:kOWSProfileManager_GroupWhitelistCollection];
}
}];
return @(1);
});
// Update account attributes.
//
// This may fail.
promise = promise.then(^(id value) {
return [self.tsAccountManager updateAccountAttributes];
});
promise = promise.then(^(id value) {
[[NSNotificationCenter defaultCenter] postNotificationNameAsync:kNSNotificationName_ProfileKeyDidChange
object:nil
userInfo:nil];
success();
});
promise = promise.catch(^(NSError *error) {
if ([error isKindOfClass:[NSError class]]) {
failure(error);
} else {
failure(OWSErrorMakeAssertionError(@"Profile key rotation failure missing error."));
}
});
[promise retainUntilComplete];
});
}
- (void)regenerateLocalProfile
{
OWSUserProfile *userProfile = self.localUserProfile;
[userProfile clearWithProfileKey:[OWSAES256Key generateRandomKey] dbConnection:self.dbConnection completion:nil];
[[self.tsAccountManager updateAccountAttributes] retainUntilComplete];
NSString *userPublicKey = [SNGeneralUtilities getUserPublicKey];
SNContact *contact = [LKStorage.shared getContactWithSessionID:userPublicKey];
contact.profilePictureEncryptionKey = [OWSAES256Key generateRandomKey];
contact.profilePictureURL = nil;
contact.profilePictureFileName = nil;
[LKStorage writeWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
[LKStorage.shared setContact:contact usingTransaction:transaction];
} completion:^{
[[self.tsAccountManager updateAccountAttributes] retainUntilComplete];
}];
}
#pragma mark - Other Users' Profiles
@ -662,22 +384,27 @@ typedef void (^ProfileManagerFailureBlock)(NSError *error);
return;
}
OWSUserProfile *userProfile = [OWSUserProfile getOrBuildUserProfileForRecipientId:recipientId dbConnection:self.dbConnection];
SNContact *contact = [LKStorage.shared getContactWithSessionID:recipientId];
OWSAssertDebug(userProfile);
if (userProfile.profileKey && [userProfile.profileKey.keyData isEqual:profileKey.keyData]) {
OWSAssertDebug(contact);
if (contact.profilePictureEncryptionKey != nil && [contact.profilePictureEncryptionKey.keyData isEqual:profileKey.keyData]) {
// Ignore redundant update.
return;
}
[userProfile clearWithProfileKey:profileKey
dbConnection:self.dbConnection
completion:^{
dispatch_async(dispatch_get_main_queue(), ^{
[userProfile updateWithAvatarUrlPath:avatarURL avatarFileName:nil dbConnection:self.dbConnection completion:^{
[self downloadAvatarForUserProfile:userProfile];
}];
});
contact.profilePictureEncryptionKey = profileKey;
contact.profilePictureURL = nil;
contact.profilePictureFileName = nil;
[LKStorage writeWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
[LKStorage.shared setContact:contact usingTransaction:transaction];
} completion:^{
contact.profilePictureURL = avatarURL;
[LKStorage writeWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
[LKStorage.shared setContact:contact usingTransaction:transaction];
} completion:^{
[self downloadAvatarForUserProfile:contact];
}];
}];
});
}
@ -696,42 +423,24 @@ typedef void (^ProfileManagerFailureBlock)(NSError *error);
{
OWSAssertDebug(recipientId.length > 0);
// For "local reads", use the local user profile.
OWSUserProfile *userProfile = ([self.tsAccountManager.localNumber isEqualToString:recipientId]
? self.localUserProfile
: [OWSUserProfile getOrBuildUserProfileForRecipientId:recipientId dbConnection:self.dbConnection]);
OWSAssertDebug(userProfile);
SNContact *contact = [LKStorage.shared getContactWithSessionID:recipientId];
OWSAssertDebug(contact);
return userProfile.profileKey;
}
- (nullable NSString *)profileNameForRecipientWithID:(NSString *)recipientID transaction:(YapDatabaseReadWriteTransaction *)transaction
{
OWSAssertDebug(recipientID.length > 0);
// For "local reads", use the local user profile.
OWSUserProfile *userProfile = [self.tsAccountManager.localNumber isEqualToString:recipientID]
? [self getLocalUserProfileWithTransaction:transaction]
: [OWSUserProfile getOrBuildUserProfileForRecipientId:recipientID transaction:transaction];
return userProfile.profileName;
return contact.profilePictureEncryptionKey;
}
- (nullable UIImage *)profileAvatarForRecipientId:(NSString *)recipientId
{
OWSAssertDebug(recipientId.length > 0);
// For "local reads", use the local user profile.
OWSUserProfile *userProfile = ([self.tsAccountManager.localNumber isEqualToString:recipientId]
? self.localUserProfile
: [OWSUserProfile getOrBuildUserProfileForRecipientId:recipientId dbConnection:self.dbConnection]);
SNContact *contact = [LKStorage.shared getContactWithSessionID:recipientId];
if (userProfile.avatarFileName.length > 0) {
return [self loadProfileAvatarWithFilename:userProfile.avatarFileName];
if (contact.profilePictureFileName != nil && contact.profilePictureFileName.length > 0) {
return [self loadProfileAvatarWithFilename:contact.profilePictureFileName];
}
if (userProfile.avatarUrlPath.length > 0) {
[self downloadAvatarForUserProfile:userProfile];
if (contact.profilePictureURL != nil && contact.profilePictureURL.length > 0) {
[self downloadAvatarForUserProfile:contact];
}
return nil;
@ -741,13 +450,10 @@ typedef void (^ProfileManagerFailureBlock)(NSError *error);
{
OWSAssertDebug(recipientId.length > 0);
// For "local reads", use the local user profile.
OWSUserProfile *userProfile = ([self.tsAccountManager.localNumber isEqualToString:recipientId]
? self.localUserProfile
: [OWSUserProfile getOrBuildUserProfileForRecipientId:recipientId dbConnection:self.dbConnection]);
SNContact *contact = [LKStorage.shared getContactWithSessionID:recipientId];
if (userProfile.avatarFileName.length > 0) {
return [self loadProfileDataWithFilename:userProfile.avatarFileName];
if (contact.profilePictureFileName != nil && contact.profilePictureFileName.length > 0) {
return [self loadProfileDataWithFilename:contact.profilePictureFileName];
}
return nil;
@ -758,40 +464,42 @@ typedef void (^ProfileManagerFailureBlock)(NSError *error);
return [[NSUUID UUID].UUIDString stringByAppendingPathExtension:@"jpg"];
}
- (void)downloadAvatarForUserProfile:(OWSUserProfile *)userProfile
- (void)downloadAvatarForUserProfile:(SNContact *)contact
{
OWSAssertDebug(userProfile);
OWSAssertDebug(contact);
__block OWSBackgroundTask *backgroundTask = [OWSBackgroundTask backgroundTaskWithLabelStr:__PRETTY_FUNCTION__];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
if (userProfile.avatarUrlPath.length < 1) {
OWSLogDebug(@"Skipping downloading avatar for %@ because url is not set", userProfile.recipientId);
BOOL hasProfilePictureURL = (contact.profilePictureURL != nil && contact.profilePictureURL.length > 0);
if (!hasProfilePictureURL) {
OWSLogDebug(@"Skipping downloading avatar for %@ because url is not set", contact.sessionID);
return;
}
NSString *_Nullable avatarUrlPathAtStart = userProfile.avatarUrlPath;
NSString *_Nullable avatarUrlPathAtStart = contact.profilePictureURL;
if (userProfile.profileKey.keyData.length < 1 || userProfile.avatarUrlPath.length < 1) {
BOOL hasProfileEncryptionKey = (contact.profilePictureEncryptionKey != nil && contact.profilePictureEncryptionKey.keyData.length > 0);
if (!hasProfileEncryptionKey || !hasProfilePictureURL) {
return;
}
OWSAES256Key *profileKeyAtStart = userProfile.profileKey;
OWSAES256Key *profileKeyAtStart = contact.profilePictureEncryptionKey;
NSString *fileName = [self generateAvatarFilename];
NSString *filePath = [OWSUserProfile profileAvatarFilepathWithFilename:fileName];
@synchronized(self.currentAvatarDownloads)
{
if ([self.currentAvatarDownloads containsObject:userProfile.recipientId]) {
if ([self.currentAvatarDownloads containsObject:contact.sessionID]) {
// Download already in flight; ignore.
return;
}
[self.currentAvatarDownloads addObject:userProfile.recipientId];
[self.currentAvatarDownloads addObject:contact.sessionID];
}
OWSLogVerbose(@"downloading profile avatar: %@", userProfile.uniqueId);
OWSLogVerbose(@"downloading profile avatar: %@", contact.sessionID);
NSString *profilePictureURL = userProfile.avatarUrlPath;
NSString *profilePictureURL = contact.profilePictureURL;
NSString *file = [profilePictureURL lastPathComponent];
BOOL useOldServer = [profilePictureURL containsString:SNFileServerAPIV2.oldServer];
@ -800,7 +508,7 @@ typedef void (^ProfileManagerFailureBlock)(NSError *error);
[promise.then(^(NSData *data) {
@synchronized(self.currentAvatarDownloads)
{
[self.currentAvatarDownloads removeObject:userProfile.recipientId];
[self.currentAvatarDownloads removeObject:contact.sessionID];
}
NSData *_Nullable encryptedData = data;
NSData *_Nullable decryptedData = [self decryptProfileData:encryptedData profileKey:profileKeyAtStart];
@ -812,24 +520,28 @@ typedef void (^ProfileManagerFailureBlock)(NSError *error);
}
}
OWSUserProfile *latestUserProfile = [OWSUserProfile getOrBuildUserProfileForRecipientId:userProfile.recipientId dbConnection:self.dbConnection];
SNContact *latestContact = [LKStorage.shared getContactWithSessionID:contact.sessionID];
if (latestUserProfile.profileKey.keyData.length < 1
|| ![latestUserProfile.profileKey isEqual:userProfile.profileKey]) {
BOOL hasProfileEncryptionKey = (latestContact.profilePictureEncryptionKey != nil
&& latestContact.profilePictureEncryptionKey.keyData.length > 0);
if (!hasProfileEncryptionKey || ![latestContact.profilePictureEncryptionKey isEqual:contact.profilePictureEncryptionKey]) {
OWSLogWarn(@"Ignoring avatar download for obsolete user profile.");
} else if (![avatarUrlPathAtStart isEqualToString:latestUserProfile.avatarUrlPath]) {
} else if (![avatarUrlPathAtStart isEqualToString:latestContact.profilePictureURL]) {
OWSLogInfo(@"avatar url has changed during download");
if (latestUserProfile.avatarUrlPath.length > 0) {
[self downloadAvatarForUserProfile:latestUserProfile];
if (latestContact.profilePictureURL != nil && latestContact.profilePictureURL.length > 0) {
[self downloadAvatarForUserProfile:latestContact];
}
} else if (!encryptedData) {
OWSLogError(@"avatar encrypted data for %@ could not be read.", userProfile.recipientId);
OWSLogError(@"avatar encrypted data for %@ could not be read.", contact.sessionID);
} else if (!decryptedData) {
OWSLogError(@"avatar data for %@ could not be decrypted.", userProfile.recipientId);
OWSLogError(@"avatar data for %@ could not be decrypted.", contact.sessionID);
} else if (!image) {
OWSLogError(@"avatar image for %@ could not be loaded.", userProfile.recipientId);
OWSLogError(@"avatar image for %@ could not be loaded.", contact.sessionID);
} else {
[latestUserProfile updateWithAvatarFileName:fileName dbConnection:self.dbConnection completion:nil];
latestContact.profilePictureFileName = fileName;
[LKStorage writeWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
[LKStorage.shared setContact:latestContact usingTransaction:transaction];
}];
[self updateProfileAvatarCache:image filename:fileName];
}
@ -849,64 +561,28 @@ typedef void (^ProfileManagerFailureBlock)(NSError *error);
// Ensure decryption, etc. off main thread.
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
OWSUserProfile *userProfile =
[OWSUserProfile getOrBuildUserProfileForRecipientId:recipientId dbConnection:self.dbConnection];
SNContact *contact = [LKStorage.shared getContactWithSessionID:recipientId];
NSString *_Nullable localNumber = self.tsAccountManager.localNumber;
// If we're updating the profile that corresponds to our local number,
// make sure we're using the latest key.
if (localNumber && [localNumber isEqualToString:recipientId]) {
[userProfile updateWithProfileKey:self.localUserProfile.profileKey
dbConnection:self.dbConnection
completion:nil];
}
if (!userProfile.profileKey) {
return;
}
if (!contact.profilePictureEncryptionKey) { return; }
NSString *_Nullable profileName =
[self decryptProfileNameData:profileNameEncrypted profileKey:userProfile.profileKey];
[self decryptProfileNameData:profileNameEncrypted profileKey:contact.profilePictureEncryptionKey];
[userProfile updateWithProfileName:profileName
avatarUrlPath:avatarUrlPath
dbConnection:self.dbConnection
completion:nil];
// If we're updating the profile that corresponds to our local number,
// update the local profile as well.
if (localNumber && [localNumber isEqualToString:recipientId]) {
OWSUserProfile *localUserProfile = self.localUserProfile;
OWSAssertDebug(localUserProfile);
[localUserProfile updateWithProfileName:profileName
avatarUrlPath:avatarUrlPath
dbConnection:self.dbConnection
completion:nil];
}
contact.name = profileName;
contact.profilePictureURL = avatarUrlPath;
// 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 hasProfilePictureURL = (contact.profilePictureURL != nil && contact.profilePictureURL.length > 0);
BOOL hasProfilePictureFileName = (contact.profilePictureFileName != nil && contact.profilePictureFileName.length > 0);
if (hasProfilePictureURL && !hasProfilePictureFileName) {
[self downloadAvatarForUserProfile:contact];
}
});
}
- (void)updateProfileForContactWithID:(NSString *)contactID displayName:(NSString *)displayName with:(YapDatabaseReadWriteTransaction *)transaction
{
OWSUserProfile *userProfile = [OWSUserProfile getOrBuildUserProfileForRecipientId:contactID transaction:transaction];
[userProfile updateWithProfileName:displayName avatarUrlPath:userProfile.avatarUrlPath avatarFileName:userProfile.avatarFileName transaction:transaction completion:nil];
}
- (void)ensureProfileCachedForContactWithID:(NSString *)contactID with:(YapDatabaseReadWriteTransaction *)transaction
{
OWSUserProfile *userProfile = [OWSUserProfile getOrBuildUserProfileForRecipientId:contactID transaction:transaction];
[userProfile saveWithTransaction:transaction];
}
- (BOOL)isNullableDataEqual:(NSData *_Nullable)left toData:(NSData *_Nullable)right
{
if (left == nil && right == nil) {
@ -983,7 +659,9 @@ typedef void (^ProfileManagerFailureBlock)(NSError *error);
- (nullable NSData *)encryptProfileData:(nullable NSData *)data
{
return [self encryptProfileData:data profileKey:self.localProfileKey];
OWSAES256Key *localProfileKey = [LKStorage.shared getUser].profilePictureEncryptionKey;
return [self encryptProfileData:data profileKey:localProfileKey];
}
- (BOOL)isProfileNameTooLong:(nullable NSString *)profileName
@ -1010,7 +688,9 @@ typedef void (^ProfileManagerFailureBlock)(NSError *error);
[paddedNameData increaseLengthBy:paddingByteCount];
OWSAssertDebug(paddedNameData.length == kOWSProfileManager_NameDataLength);
return [self encryptProfileData:[paddedNameData copy] profileKey:self.localProfileKey];
OWSAES256Key *localProfileKey = [LKStorage.shared getUser].profilePictureEncryptionKey;
return [self encryptProfileData:[paddedNameData copy] profileKey:localProfileKey];
}
#pragma mark - Avatar Disk Cache
@ -1061,16 +741,6 @@ typedef void (^ProfileManagerFailureBlock)(NSError *error);
}
}
#pragma mark - Notifications
- (void)blockListDidChange:(NSNotification *)notification {
OWSAssertIsOnMainThread();
[AppReadiness runNowOrWhenAppDidBecomeReady:^{
[self rotateLocalProfileKeyIfNecessary];
}];
}
@end
NS_ASSUME_NONNULL_END