diff --git a/Session/Meta/AppDelegate.m b/Session/Meta/AppDelegate.m index 1ab79f8e4..359320080 100644 --- a/Session/Meta/AppDelegate.m +++ b/Session/Meta/AppDelegate.m @@ -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]; diff --git a/SessionMessagingKit/Database/Storage+Contacts.swift b/SessionMessagingKit/Database/Storage+Contacts.swift index 74507c0f6..cf448694d 100644 --- a/SessionMessagingKit/Database/Storage+Contacts.swift +++ b/SessionMessagingKit/Database/Storage+Contacts.swift @@ -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 { + @objc public func getAllContacts() -> Set { var result: Set = [] Storage.read { transaction in transaction.enumerateRows(inCollection: Storage.contactCollection) { _, object, _, _ in diff --git a/SessionMessagingKit/Database/Storage+Shared.swift b/SessionMessagingKit/Database/Storage+Shared.swift index 4562f4abf..17de9059d 100644 --- a/SessionMessagingKit/Database/Storage+Shared.swift +++ b/SessionMessagingKit/Database/Storage+Shared.swift @@ -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 } diff --git a/SessionMessagingKit/Sending & Receiving/MessageReceiver+Handling.swift b/SessionMessagingKit/Sending & Receiving/MessageReceiver+Handling.swift index 33991b62c..5fc0a968b 100644 --- a/SessionMessagingKit/Sending & Receiving/MessageReceiver+Handling.swift +++ b/SessionMessagingKit/Sending & Receiving/MessageReceiver+Handling.swift @@ -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) } diff --git a/SessionMessagingKit/Storage.swift b/SessionMessagingKit/Storage.swift index 9753d82bb..4444307d3 100644 --- a/SessionMessagingKit/Storage.swift +++ b/SessionMessagingKit/Storage.swift @@ -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 // MARK: - Closed Groups diff --git a/SessionMessagingKit/To Do/OWSUserProfile.h b/SessionMessagingKit/To Do/OWSUserProfile.h index 9d2592aa8..b45356ac5 100644 --- a/SessionMessagingKit/To Do/OWSUserProfile.h +++ b/SessionMessagingKit/To Do/OWSUserProfile.h @@ -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; diff --git a/SessionMessagingKit/To Do/OWSUserProfile.m b/SessionMessagingKit/To Do/OWSUserProfile.m index 098c8e62f..b01b70503 100644 --- a/SessionMessagingKit/To Do/OWSUserProfile.m +++ b/SessionMessagingKit/To Do/OWSUserProfile.m @@ -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 *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 *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]; } diff --git a/SessionMessagingKit/To Do/ProfileManagerProtocol.h b/SessionMessagingKit/To Do/ProfileManagerProtocol.h index 3b1d29131..6dfb991ab 100644 --- a/SessionMessagingKit/To Do/ProfileManagerProtocol.h +++ b/SessionMessagingKit/To Do/ProfileManagerProtocol.h @@ -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 diff --git a/SessionMessagingKit/Utilities/FullTextSearchFinder.swift b/SessionMessagingKit/Utilities/FullTextSearchFinder.swift index 05ae92508..96b112024 100644 --- a/SessionMessagingKit/Utilities/FullTextSearchFinder.swift +++ b/SessionMessagingKit/Utilities/FullTextSearchFinder.swift @@ -177,7 +177,7 @@ public class FullTextSearchFinder: NSObject { } private static let recipientIndexer: SearchIndexer = 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)" } diff --git a/SessionShareExtension/ShareVC.swift b/SessionShareExtension/ShareVC.swift index 78439aa88..41cfa31a7 100644 --- a/SessionShareExtension/ShareVC.swift +++ b/SessionShareExtension/ShareVC.swift @@ -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 diff --git a/SignalUtilitiesKit/Database/Storage+Conformances.swift b/SignalUtilitiesKit/Database/Storage+Conformances.swift index a9049ee21..21c74ee43 100644 --- a/SignalUtilitiesKit/Database/Storage+Conformances.swift +++ b/SignalUtilitiesKit/Database/Storage+Conformances.swift @@ -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) - } } diff --git a/SignalUtilitiesKit/Messaging/ConfigurationMessage+Convenience.swift b/SignalUtilitiesKit/Messaging/ConfigurationMessage+Convenience.swift index 618db9117..3e20fd601 100644 --- a/SignalUtilitiesKit/Messaging/ConfigurationMessage+Convenience.swift +++ b/SignalUtilitiesKit/Messaging/ConfigurationMessage+Convenience.swift @@ -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 } } diff --git a/SignalUtilitiesKit/To Do/OWSProfileManager.h b/SignalUtilitiesKit/To Do/OWSProfileManager.h index 4b653cba6..6df6b5c70 100644 --- a/SignalUtilitiesKit/To Do/OWSProfileManager.h +++ b/SignalUtilitiesKit/To Do/OWSProfileManager.h @@ -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 diff --git a/SignalUtilitiesKit/To Do/OWSProfileManager.m b/SignalUtilitiesKit/To Do/OWSProfileManager.m index e50b7aeb9..5395455dd 100644 --- a/SignalUtilitiesKit/To Do/OWSProfileManager.m +++ b/SignalUtilitiesKit/To Do/OWSProfileManager.m @@ -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 *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]; - } - - successBlock(); - }]; + userProfile.name = profileName; + userProfile.profilePictureURL = avatarUrlPath; + userProfile.profilePictureFileName = avatarFileName; + + [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 *whitelistedRecipientIds = [NSMutableSet new]; - NSMutableSet *whitelistedGroupIds = [NSMutableSet new]; - [self.dbConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) { - [whitelistedRecipientIds - addObjectsFromArray:[transaction allKeysInCollection:kOWSProfileManager_UserWhitelistCollection]]; - - NSArray *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 *blockedRecipientIds = [NSSet setWithArray:self.blockingManager.blockedPhoneNumbers]; - NSSet *blockedGroupIds = [NSSet setWithArray:self.blockingManager.blockedGroupIds]; - - // Find the users and groups which are both a) blocked b) may have our current profile key. - NSMutableSet *intersectingRecipientIds = [blockedRecipientIds mutableCopy]; - [intersectingRecipientIds intersectSet:whitelistedRecipientIds]; - NSMutableSet *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 *)intersectingRecipientIds - intersectingGroupIds:(NSSet *)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]); - - if (userProfile.avatarFileName.length > 0) { - return [self loadProfileAvatarWithFilename:userProfile.avatarFileName]; + SNContact *contact = [LKStorage.shared getContactWithSessionID:recipientId]; + + 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]; - - [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]; - } + [self decryptProfileNameData:profileNameEncrypted profileKey:contact.profilePictureEncryptionKey]; + + 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