diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index f03e8f12e..d3428122f 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -5063,7 +5063,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 273; + CURRENT_PROJECT_VERSION = 276; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = SUQ8J2PCT7; FRAMEWORK_SEARCH_PATHS = "$(inherited)"; @@ -5132,7 +5132,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 273; + CURRENT_PROJECT_VERSION = 276; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = SUQ8J2PCT7; ENABLE_NS_ASSERTIONS = NO; @@ -5193,7 +5193,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 273; + CURRENT_PROJECT_VERSION = 276; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = SUQ8J2PCT7; FRAMEWORK_SEARCH_PATHS = "$(inherited)"; @@ -5263,7 +5263,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 273; + CURRENT_PROJECT_VERSION = 276; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = SUQ8J2PCT7; ENABLE_NS_ASSERTIONS = NO; @@ -6148,7 +6148,7 @@ CODE_SIGN_ENTITLEMENTS = Session/Meta/Signal.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 273; + CURRENT_PROJECT_VERSION = 276; DEVELOPMENT_TEAM = SUQ8J2PCT7; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -6216,7 +6216,7 @@ CODE_SIGN_ENTITLEMENTS = Session/Meta/Signal.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 273; + CURRENT_PROJECT_VERSION = 276; DEVELOPMENT_TEAM = SUQ8J2PCT7; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", 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/Session/Onboarding/Onboarding.swift b/Session/Onboarding/Onboarding.swift index 2166affe7..924536923 100644 --- a/Session/Onboarding/Onboarding.swift +++ b/Session/Onboarding/Onboarding.swift @@ -8,7 +8,12 @@ enum Onboarding { func preregister(with seed: Data, ed25519KeyPair: Sign.KeyPair, x25519KeyPair: ECKeyPair) { let userDefaults = UserDefaults.standard KeyPairUtilities.store(seed: seed, ed25519KeyPair: ed25519KeyPair, x25519KeyPair: x25519KeyPair) - TSAccountManager.sharedInstance().phoneNumberAwaitingVerification = x25519KeyPair.hexEncodedPublicKey + let x25519PublicKey = x25519KeyPair.hexEncodedPublicKey + TSAccountManager.sharedInstance().phoneNumberAwaitingVerification = x25519PublicKey + Storage.writeSync { transaction in + let user = Contact(sessionID: x25519PublicKey) + Storage.shared.setContact(user, using: transaction) + } switch self { case .register: userDefaults[.hasViewedSeed] = false diff --git a/SessionMessagingKit/Contacts/Contact.swift b/SessionMessagingKit/Contacts/Contact.swift index f909a6888..ada7fc479 100644 --- a/SessionMessagingKit/Contacts/Contact.swift +++ b/SessionMessagingKit/Contacts/Contact.swift @@ -6,8 +6,8 @@ public class Contact : NSObject, NSCoding { // NSObject/NSCoding conformance is @objc public var profilePictureURL: String? /// The file name of the contact's profile picture on local storage. @objc public var profilePictureFileName: String? - /// The key with which the profile picture is encrypted. - @objc public var profilePictureEncryptionKey: OWSAES256Key? + /// The key with which the profile is encrypted. + @objc public var profileEncryptionKey: OWSAES256Key? /// The ID of the thread associated with this contact. @objc public var threadID: String? /// This flag is used to determine whether we should auto-download files sent by this contact. @@ -49,8 +49,8 @@ public class Contact : NSObject, NSCoding { // NSObject/NSCoding conformance is // MARK: Validation public var isValid: Bool { - if profilePictureURL != nil { return (profilePictureEncryptionKey != nil) } - if profilePictureEncryptionKey != nil { return (profilePictureURL != nil) } + if profilePictureURL != nil { return (profileEncryptionKey != nil) } + if profileEncryptionKey != nil { return (profilePictureURL != nil) } return true } @@ -63,7 +63,7 @@ public class Contact : NSObject, NSCoding { // NSObject/NSCoding conformance is if let nickname = coder.decodeObject(forKey: "nickname") as! String? { self.nickname = nickname } if let profilePictureURL = coder.decodeObject(forKey: "profilePictureURL") as! String? { self.profilePictureURL = profilePictureURL } if let profilePictureFileName = coder.decodeObject(forKey: "profilePictureFileName") as! String? { self.profilePictureFileName = profilePictureFileName } - if let profilePictureEncryptionKey = coder.decodeObject(forKey: "profilePictureEncryptionKey") as! OWSAES256Key? { self.profilePictureEncryptionKey = profilePictureEncryptionKey } + if let profileEncryptionKey = coder.decodeObject(forKey: "profilePictureEncryptionKey") as! OWSAES256Key? { self.profileEncryptionKey = profileEncryptionKey } if let threadID = coder.decodeObject(forKey: "threadID") as! String? { self.threadID = threadID } } @@ -73,7 +73,7 @@ public class Contact : NSObject, NSCoding { // NSObject/NSCoding conformance is coder.encode(nickname, forKey: "nickname") coder.encode(profilePictureURL, forKey: "profilePictureURL") coder.encode(profilePictureFileName, forKey: "profilePictureFileName") - coder.encode(profilePictureEncryptionKey, forKey: "profilePictureEncryptionKey") + coder.encode(profileEncryptionKey, forKey: "profilePictureEncryptionKey") coder.encode(threadID, forKey: "threadID") coder.encode(isTrusted, forKey: "isTrusted") } diff --git a/SessionMessagingKit/Database/Storage+Contacts.swift b/SessionMessagingKit/Database/Storage+Contacts.swift index 74507c0f6..04f3cf128 100644 --- a/SessionMessagingKit/Database/Storage+Contacts.swift +++ b/SessionMessagingKit/Database/Storage+Contacts.swift @@ -17,12 +17,22 @@ 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, + oldProfilePictureFileName != contact.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 +44,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..ffebdd830 100644 --- a/SessionMessagingKit/Sending & Receiving/MessageReceiver+Handling.swift +++ b/SessionMessagingKit/Sending & Receiving/MessageReceiver+Handling.swift @@ -181,24 +181,20 @@ 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) - } // 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.profileEncryptionKey = 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 +298,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 +310,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) { + if let profileKey = profileKey, let profilePictureURL = profilePictureURL, + profileKey.keyData.count == kAES256_KeyByteLength, profileKey != contact.profileEncryptionKey { let shouldUpdate: Bool if isCurrentUser { shouldUpdate = given(userDefaults[.lastProfilePictureUpdate]) { sentTimestamp > UInt64($0.timeIntervalSince1970 * 1000) } ?? true @@ -336,21 +326,18 @@ 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 + contact.profileEncryptionKey = 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) + // Download the profile picture if needed + transaction.addCompletionQueue(DispatchQueue.main) { + SSKEnvironment.shared.profileManager.downloadAvatar(forUserProfile: contact) + } } diff --git a/SessionMessagingKit/Sending & Receiving/MessageSender.swift b/SessionMessagingKit/Sending & Receiving/MessageSender.swift index 089ea6a34..456ba6e23 100644 --- a/SessionMessagingKit/Sending & Receiving/MessageSender.swift +++ b/SessionMessagingKit/Sending & Receiving/MessageSender.swift @@ -145,7 +145,7 @@ public final class MessageSender : NSObject { // Attach the user's profile if needed if let message = message as? VisibleMessage { guard let name = storage.getUser()?.name else { handleFailure(with: Error.noUsername, using: transaction); return promise } - if let profileKey = storage.getUser()?.profilePictureEncryptionKey?.keyData, let profilePictureURL = storage.getUser()?.profilePictureURL { + if let profileKey = storage.getUser()?.profileEncryptionKey?.keyData, let profilePictureURL = storage.getUser()?.profilePictureURL { message.profile = VisibleMessage.Profile(displayName: name, profileKey: profileKey, profilePictureURL: profilePictureURL) } else { message.profile = VisibleMessage.Profile(displayName: name) @@ -200,7 +200,8 @@ public final class MessageSender : NSObject { } // Send the result let base64EncodedData = wrappedMessage.base64EncodedString() - let snodeMessage = SnodeMessage(recipient: message.recipient!, data: base64EncodedData, ttl: message.ttl, timestamp: message.sentTimestamp!) + let timestamp = UInt64(Int64(message.sentTimestamp!) + SnodeAPI.clockOffset) + let snodeMessage = SnodeMessage(recipient: message.recipient!, data: base64EncodedData, ttl: message.ttl, timestamp: timestamp) SnodeAPI.sendMessage(snodeMessage).done(on: DispatchQueue.global(qos: .userInitiated)) { promises in var isSuccess = false let promiseCount = promises.count @@ -285,7 +286,7 @@ public final class MessageSender : NSObject { guard message.isValid else { handleFailure(with: Error.invalidMessage, using: transaction); return promise } // Attach the user's profile guard let name = storage.getUser()?.name else { handleFailure(with: Error.noUsername, using: transaction); return promise } - if let profileKey = storage.getUser()?.profilePictureEncryptionKey?.keyData, let profilePictureURL = storage.getUser()?.profilePictureURL { + if let profileKey = storage.getUser()?.profileEncryptionKey?.keyData, let profilePictureURL = storage.getUser()?.profilePictureURL { message.profile = VisibleMessage.Profile(displayName: name, profileKey: profileKey, profilePictureURL: profilePictureURL) } else { message.profile = VisibleMessage.Profile(displayName: name) 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/SessionMessagingKit/Utilities/ProtoUtils.m b/SessionMessagingKit/Utilities/ProtoUtils.m index e1a4d4bcf..0a1ac6874 100644 --- a/SessionMessagingKit/Utilities/ProtoUtils.m +++ b/SessionMessagingKit/Utilities/ProtoUtils.m @@ -21,7 +21,7 @@ NS_ASSUME_NONNULL_BEGIN + (OWSAES256Key *)localProfileKey { - return [[LKStorage.shared getUser] profilePictureEncryptionKey]; + return [[LKStorage.shared getUser] profileEncryptionKey]; } #pragma mark - 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/SessionSnodeKit/OnionRequestAPI.swift b/SessionSnodeKit/OnionRequestAPI.swift index dc04ae57e..61e617c72 100644 --- a/SessionSnodeKit/OnionRequestAPI.swift +++ b/SessionSnodeKit/OnionRequestAPI.swift @@ -310,7 +310,7 @@ public enum OnionRequestAPI { } /// Sends an onion request to `server`. Builds new paths as needed. - public static func sendOnionRequest(_ request: NSURLRequest, to server: String, target: String = "/loki/v3/lsrpc", using x25519PublicKey: String, isJSONRequired: Bool = true) -> Promise { + public static func sendOnionRequest(_ request: NSURLRequest, to server: String, target: String = "/loki/v3/lsrpc", using x25519PublicKey: String) -> Promise { var rawHeaders = request.allHTTPHeaderFields ?? [:] rawHeaders.removeValue(forKey: "User-Agent") var headers: JSON = rawHeaders.mapValues { value in @@ -352,14 +352,14 @@ public enum OnionRequestAPI { "headers" : headers ] let destination = Destination.server(host: host, target: target, x25519PublicKey: x25519PublicKey, scheme: scheme, port: port) - let promise = sendOnionRequest(with: payload, to: destination, isJSONRequired: isJSONRequired) + let promise = sendOnionRequest(with: payload, to: destination) promise.catch2 { error in SNLog("Couldn't reach server: \(url) due to error: \(error).") } return promise } - public static func sendOnionRequest(with payload: JSON, to destination: Destination, isJSONRequired: Bool = true) -> Promise { + public static func sendOnionRequest(with payload: JSON, to destination: Destination) -> Promise { let (promise, seal) = Promise.pending() var guardSnode: Snode? Threading.workQueue.async { // Avoid race conditions on `guardSnodes` and `paths` @@ -386,28 +386,26 @@ public enum OnionRequestAPI { let ivAndCiphertext = Data(base64Encoded: base64EncodedIVAndCiphertext), ivAndCiphertext.count >= AESGCM.ivSize else { return seal.reject(HTTP.Error.invalidJSON) } do { let data = try AESGCM.decrypt(ivAndCiphertext, with: destinationSymmetricKey) - // The old open group server and file server implementations put the status code in the JSON under the "status" - // key, whereas the new implementations put it under the "status_code" key guard let json = try JSONSerialization.jsonObject(with: data, options: [ .fragmentsAllowed ]) as? JSON, let statusCode = json["status_code"] as? Int ?? json["status"] as? Int else { return seal.reject(HTTP.Error.invalidJSON) } if statusCode == 406 { // Clock out of sync SNLog("The user's clock is out of sync with the service node network.") seal.reject(SnodeAPI.Error.clockOutOfSync) } else if let bodyAsString = json["body"] as? String { - // This clause is only used by the old open group and file server implementations. The new implementations will - // always go to the next clause. - let body: JSON - if !isJSONRequired { - body = [ "result" : bodyAsString ] - } else { - guard let bodyAsData = bodyAsString.data(using: .utf8), - let b = try JSONSerialization.jsonObject(with: bodyAsData, options: [ .fragmentsAllowed ]) as? JSON else { return seal.reject(HTTP.Error.invalidJSON) } - body = b + guard let bodyAsData = bodyAsString.data(using: .utf8), + let body = try JSONSerialization.jsonObject(with: bodyAsData, options: [ .fragmentsAllowed ]) as? JSON else { return seal.reject(HTTP.Error.invalidJSON) } + if let timestamp = body["t"] as? Int64 { + let offset = timestamp - Int64(NSDate.millisecondTimestamp()) + SnodeAPI.clockOffset = offset + } + guard 200...299 ~= statusCode else { + return seal.reject(Error.httpRequestFailedAtDestination(statusCode: UInt(statusCode), json: body, destination: destination)) } - guard 200...299 ~= statusCode else { return seal.reject(Error.httpRequestFailedAtDestination(statusCode: UInt(statusCode), json: body, destination: destination)) } seal.fulfill(body) } else { - guard 200...299 ~= statusCode else { return seal.reject(Error.httpRequestFailedAtDestination(statusCode: UInt(statusCode), json: json, destination: destination)) } + guard 200...299 ~= statusCode else { + return seal.reject(Error.httpRequestFailedAtDestination(statusCode: UInt(statusCode), json: json, destination: destination)) + } seal.fulfill(json) } } catch { diff --git a/SessionSnodeKit/SnodeAPI.swift b/SessionSnodeKit/SnodeAPI.swift index 214bef8f9..50c12c59d 100644 --- a/SessionSnodeKit/SnodeAPI.swift +++ b/SessionSnodeKit/SnodeAPI.swift @@ -13,6 +13,11 @@ public final class SnodeAPI : NSObject { /// - Note: Should only be accessed from `Threading.workQueue` to avoid race conditions. internal static var snodePool: Set = [] + /// The offset between the user's clock and the Service Node's clock. Used in cases where the + /// user's clock is incorrect. + /// + /// - Note: Should only be accessed from `Threading.workQueue` to avoid race conditions. + public static var clockOffset: Int64 = 0 /// - Note: Should only be accessed from `Threading.workQueue` to avoid race conditions. public static var swarmCache: [String:Set] = [:] 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..ac4d6da5d 100644 --- a/SignalUtilitiesKit/Messaging/ConfigurationMessage+Convenience.swift +++ b/SignalUtilitiesKit/Messaging/ConfigurationMessage+Convenience.swift @@ -6,7 +6,7 @@ extension ConfigurationMessage { guard let user = storage.getUser() else { return nil } let displayName = user.name let profilePictureURL = user.profilePictureURL - let profileKey = user.profilePictureEncryptionKey?.keyData + let profileKey = user.profileEncryptionKey?.keyData var closedGroups: Set = [] var openGroups: Set = [] var contacts: Set = [] @@ -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.profileEncryptionKey?.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..729049c36 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.profileEncryptionKey = 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.profileEncryptionKey = 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.profileEncryptionKey = 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.profileEncryptionKey = [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.profileEncryptionKey != nil && [contact.profileEncryptionKey.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.profileEncryptionKey = 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.profileEncryptionKey; } - (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.profileEncryptionKey != nil && contact.profileEncryptionKey.keyData.length > 0); + if (!hasProfileEncryptionKey || !hasProfilePictureURL) { return; } - OWSAES256Key *profileKeyAtStart = userProfile.profileKey; + OWSAES256Key *profileKeyAtStart = contact.profileEncryptionKey; 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.profileEncryptionKey != nil + && latestContact.profileEncryptionKey.keyData.length > 0); + if (!hasProfileEncryptionKey || ![latestContact.profileEncryptionKey isEqual:contact.profileEncryptionKey]) { 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,32 @@ 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.profileEncryptionKey) { 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.profileEncryptionKey]; + + contact.name = profileName; + contact.profilePictureURL = avatarUrlPath; + + [LKStorage writeWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { + [LKStorage.shared setContact:contact usingTransaction:transaction]; + }]; // 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 +663,9 @@ typedef void (^ProfileManagerFailureBlock)(NSError *error); - (nullable NSData *)encryptProfileData:(nullable NSData *)data { - return [self encryptProfileData:data profileKey:self.localProfileKey]; + OWSAES256Key *localProfileKey = [LKStorage.shared getUser].profileEncryptionKey; + + return [self encryptProfileData:data profileKey:localProfileKey]; } - (BOOL)isProfileNameTooLong:(nullable NSString *)profileName @@ -1010,7 +692,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].profileEncryptionKey; + + return [self encryptProfileData:[paddedNameData copy] profileKey:localProfileKey]; } #pragma mark - Avatar Disk Cache @@ -1061,16 +745,6 @@ typedef void (^ProfileManagerFailureBlock)(NSError *error); } } -#pragma mark - Notifications - -- (void)blockListDidChange:(NSNotification *)notification { - OWSAssertIsOnMainThread(); - - [AppReadiness runNowOrWhenAppDidBecomeReady:^{ - [self rotateLocalProfileKeyIfNecessary]; - }]; -} - @end NS_ASSUME_NONNULL_END