diff --git a/Session/Meta/AppDelegate.swift b/Session/Meta/AppDelegate.swift index d8064f1f6..a6815f954 100644 --- a/Session/Meta/AppDelegate.swift +++ b/Session/Meta/AppDelegate.swift @@ -7,10 +7,11 @@ extension AppDelegate { guard Storage.shared.getUser()?.name != nil else { return } let userDefaults = UserDefaults.standard let lastSync = userDefaults[.lastConfigurationSync] ?? .distantPast - guard Date().timeIntervalSince(lastSync) > 7 * 24 * 60 * 60, - let configurationMessage = ConfigurationMessage.getCurrent() else { return } // Sync every 2 days + guard Date().timeIntervalSince(lastSync) > 7 * 24 * 60 * 60 else { return } // Sync every 2 days let destination = Message.Destination.contact(publicKey: getUserHexEncodedPublicKey()) - Storage.shared.write { transaction in + Storage.write { transaction in + guard let configurationMessage = ConfigurationMessage.getCurrent(with: transaction) else { return } + let job = MessageSendJob(message: configurationMessage, destination: destination) JobQueue.shared.add(job, using: transaction) } @@ -28,7 +29,7 @@ extension AppDelegate { // Note: SQLite only supports a single write thread so we can be sure this will retrieve the most up-to-date data Storage.writeSync { transaction in - guard Storage.shared.getUser()?.name != nil, let configurationMessage = ConfigurationMessage.getCurrent() else { + guard Storage.shared.getUser()?.name != nil, let configurationMessage = ConfigurationMessage.getCurrent(with: transaction) else { seal.fulfill(()) return } diff --git a/SessionMessagingKit/Database/Storage+ClosedGroups.swift b/SessionMessagingKit/Database/Storage+ClosedGroups.swift index 95f4f5a4b..a21d6d551 100644 --- a/SessionMessagingKit/Database/Storage+ClosedGroups.swift +++ b/SessionMessagingKit/Database/Storage+ClosedGroups.swift @@ -10,13 +10,19 @@ extension Storage { private static let closedGroupZombieMembersCollection = "SNClosedGroupZombieMembersCollection" public func getClosedGroupEncryptionKeyPairs(for groupPublicKey: String) -> [ECKeyPair] { + var result: [ECKeyPair] = [] + Storage.read { transaction in + result = self.getClosedGroupEncryptionKeyPairs(for: groupPublicKey, using: transaction) + } + return result + } + + public func getClosedGroupEncryptionKeyPairs(for groupPublicKey: String, using transaction: YapDatabaseReadTransaction) -> [ECKeyPair] { let collection = Storage.getClosedGroupEncryptionKeyPairCollection(for: groupPublicKey) var timestampsAndKeyPairs: [(timestamp: Double, keyPair: ECKeyPair)] = [] - Storage.read { transaction in - transaction.enumerateKeysAndObjects(inCollection: collection) { key, object, _ in - guard let timestamp = Double(key), let keyPair = object as? ECKeyPair else { return } - timestampsAndKeyPairs.append((timestamp, keyPair)) - } + transaction.enumerateKeysAndObjects(inCollection: collection) { key, object, _ in + guard let timestamp = Double(key), let keyPair = object as? ECKeyPair else { return } + timestampsAndKeyPairs.append((timestamp, keyPair)) } return timestampsAndKeyPairs.sorted { $0.timestamp < $1.timestamp }.map { $0.keyPair } } @@ -24,6 +30,10 @@ extension Storage { public func getLatestClosedGroupEncryptionKeyPair(for groupPublicKey: String) -> ECKeyPair? { return getClosedGroupEncryptionKeyPairs(for: groupPublicKey).last } + + public func getLatestClosedGroupEncryptionKeyPair(for groupPublicKey: String, using transaction: YapDatabaseReadTransaction) -> ECKeyPair? { + return getClosedGroupEncryptionKeyPairs(for: groupPublicKey, using: transaction).last + } public func addClosedGroupEncryptionKeyPair(_ keyPair: ECKeyPair, for groupPublicKey: String, using transaction: Any) { let collection = Storage.getClosedGroupEncryptionKeyPairCollection(for: groupPublicKey) @@ -39,11 +49,15 @@ extension Storage { public func getUserClosedGroupPublicKeys() -> Set { var result: Set = [] Storage.read { transaction in - result = Set(transaction.allKeys(inCollection: Storage.closedGroupPublicKeyCollection)) + result = self.getUserClosedGroupPublicKeys(using: transaction) } return result } + public func getUserClosedGroupPublicKeys(using transaction: YapDatabaseReadTransaction) -> Set { + return Set(transaction.allKeys(inCollection: Storage.closedGroupPublicKeyCollection)) + } + public func addClosedGroupPublicKey(_ groupPublicKey: String, using transaction: Any) { (transaction as! YapDatabaseReadWriteTransaction).setObject(groupPublicKey, forKey: groupPublicKey, inCollection: Storage.closedGroupPublicKeyCollection) } @@ -81,4 +95,8 @@ extension Storage { public func isClosedGroup(_ publicKey: String) -> Bool { getUserClosedGroupPublicKeys().contains(publicKey) } + + public func isClosedGroup(_ publicKey: String, using transaction: YapDatabaseReadTransaction) -> Bool { + getUserClosedGroupPublicKeys(using: transaction).contains(publicKey) + } } diff --git a/SessionMessagingKit/Database/Storage+Shared.swift b/SessionMessagingKit/Database/Storage+Shared.swift index a975c2b76..d8f5de95d 100644 --- a/SessionMessagingKit/Database/Storage+Shared.swift +++ b/SessionMessagingKit/Database/Storage+Shared.swift @@ -36,12 +36,21 @@ extension Storage { } @objc public func getUser() -> Contact? { + return getUser(using: nil) + } + + public func getUser(using transaction: YapDatabaseReadTransaction?) -> Contact? { let userPublicKey = getUserHexEncodedPublicKey() var result: Contact? - Storage.read { transaction in + if let transaction = transaction { result = Storage.shared.getContact(with: userPublicKey, using: transaction) } + else { + Storage.read { transaction in + result = Storage.shared.getContact(with: userPublicKey, using: transaction) + } + } return result } } diff --git a/SessionMessagingKit/Messages/Control Messages/ConfigurationMessage+Convenience.swift b/SessionMessagingKit/Messages/Control Messages/ConfigurationMessage+Convenience.swift index 2a172ecfb..386c17ae7 100644 --- a/SessionMessagingKit/Messages/Control Messages/ConfigurationMessage+Convenience.swift +++ b/SessionMessagingKit/Messages/Control Messages/ConfigurationMessage+Convenience.swift @@ -2,9 +2,9 @@ import SessionUtilitiesKit extension ConfigurationMessage { - public static func getCurrent() -> ConfigurationMessage? { + public static func getCurrent(with transaction: YapDatabaseReadTransaction) -> ConfigurationMessage? { let storage = Storage.shared - guard let user = storage.getUser() else { return nil } + guard let user = storage.getUser(using: transaction) else { return nil } let displayName = user.name let profilePictureURL = user.profilePictureURL @@ -13,83 +13,84 @@ extension ConfigurationMessage { var openGroups: Set = [] var contacts: Set = [] - Storage.read { transaction in - TSGroupThread.enumerateCollectionObjects(with: transaction) { object, _ in - guard let thread = object as? TSGroupThread else { return } - - switch thread.groupModel.groupType { - case .closedGroup: - guard thread.isCurrentUserMemberInGroup() else { return } - - let groupID = thread.groupModel.groupId - let groupPublicKey = LKGroupUtilities.getDecodedGroupID(groupID) - - guard storage.isClosedGroup(groupPublicKey), let encryptionKeyPair = storage.getLatestClosedGroupEncryptionKeyPair(for: groupPublicKey) else { - return - } - - let closedGroup = ClosedGroup( - publicKey: groupPublicKey, - name: (thread.groupModel.groupName ?? ""), - encryptionKeyPair: encryptionKeyPair, - members: Set(thread.groupModel.groupMemberIds), - admins: Set(thread.groupModel.groupAdminIds), - expirationTimer: thread.disappearingMessagesDuration(with: transaction) - ) - closedGroups.insert(closedGroup) - - case .openGroup: - if let threadId: String = thread.uniqueId, let v2OpenGroup = storage.getV2OpenGroup(for: threadId) { - openGroups.insert("\(v2OpenGroup.server)/\(v2OpenGroup.room)?public_key=\(v2OpenGroup.publicKey)") - } - - default: break - } - } - - let currentUserPublicKey: String = getUserHexEncodedPublicKey() + TSGroupThread.enumerateCollectionObjects(with: transaction) { object, _ in + guard let thread = object as? TSGroupThread else { return } - contacts = storage.getAllContacts(with: transaction) - .filter { contact -> Bool in - let threadID = TSContactThread.threadID(fromContactSessionID: contact.sessionID) + switch thread.groupModel.groupType { + case .closedGroup: + guard thread.isCurrentUserMemberInGroup() else { return } - return ( - // Skip the current user - contact.sessionID != currentUserPublicKey && - // Contacts which have visible threads - TSContactThread.fetch(uniqueId: threadID, transaction: transaction)?.shouldBeVisible == true && ( - - // Include already approved contacts - contact.isApproved || - contact.didApproveMe || - - // Sync blocked contacts - SSKEnvironment.shared.blockingManager.isRecipientIdBlocked(contact.sessionID) - ) - ) - } - .map { contact -> ConfigurationMessage.Contact in - // Can just default the 'hasX' values to true as they will be set to this - // when converting to proto anyway - let profilePictureURL = contact.profilePictureURL - let profileKey = contact.profileEncryptionKey?.keyData + let groupID = thread.groupModel.groupId + let groupPublicKey = LKGroupUtilities.getDecodedGroupID(groupID) - return ConfigurationMessage.Contact( - publicKey: contact.sessionID, - displayName: (contact.name ?? contact.sessionID), - profilePictureURL: profilePictureURL, - profileKey: profileKey, - hasIsApproved: true, - isApproved: contact.isApproved, - hasIsBlocked: true, - isBlocked: SSKEnvironment.shared.blockingManager.isRecipientIdBlocked(contact.sessionID), - hasDidApproveMe: true, - didApproveMe: contact.didApproveMe + guard + storage.isClosedGroup(groupPublicKey, using: transaction), + let encryptionKeyPair = storage.getLatestClosedGroupEncryptionKeyPair(for: groupPublicKey, using: transaction) + else { + return + } + + let closedGroup = ClosedGroup( + publicKey: groupPublicKey, + name: (thread.groupModel.groupName ?? ""), + encryptionKeyPair: encryptionKeyPair, + members: Set(thread.groupModel.groupMemberIds), + admins: Set(thread.groupModel.groupAdminIds), + expirationTimer: thread.disappearingMessagesDuration(with: transaction) ) - } - .asSet() + closedGroups.insert(closedGroup) + + case .openGroup: + if let threadId: String = thread.uniqueId, let v2OpenGroup = storage.getV2OpenGroup(for: threadId) { + openGroups.insert("\(v2OpenGroup.server)/\(v2OpenGroup.room)?public_key=\(v2OpenGroup.publicKey)") + } + + default: break + } } + let currentUserPublicKey: String = getUserHexEncodedPublicKey() + + contacts = storage.getAllContacts(with: transaction) + .filter { contact -> Bool in + let threadID = TSContactThread.threadID(fromContactSessionID: contact.sessionID) + + return ( + // Skip the current user + contact.sessionID != currentUserPublicKey && + // Contacts which have visible threads + TSContactThread.fetch(uniqueId: threadID, transaction: transaction)?.shouldBeVisible == true && ( + + // Include already approved contacts + contact.isApproved || + contact.didApproveMe || + + // Sync blocked contacts + SSKEnvironment.shared.blockingManager.isRecipientIdBlocked(contact.sessionID) + ) + ) + } + .map { contact -> ConfigurationMessage.Contact in + // Can just default the 'hasX' values to true as they will be set to this + // when converting to proto anyway + let profilePictureURL = contact.profilePictureURL + let profileKey = contact.profileEncryptionKey?.keyData + + return ConfigurationMessage.Contact( + publicKey: contact.sessionID, + displayName: (contact.name ?? contact.sessionID), + profilePictureURL: profilePictureURL, + profileKey: profileKey, + hasIsApproved: true, + isApproved: contact.isApproved, + hasIsBlocked: true, + isBlocked: SSKEnvironment.shared.blockingManager.isRecipientIdBlocked(contact.sessionID), + hasDidApproveMe: true, + didApproveMe: contact.didApproveMe + ) + } + .asSet() + return ConfigurationMessage( displayName: displayName, profilePictureURL: profilePictureURL, diff --git a/SessionMessagingKit/Sending & Receiving/MessageReceiver+Handling.swift b/SessionMessagingKit/Sending & Receiving/MessageReceiver+Handling.swift index ab23a33f0..aa00ecba8 100644 --- a/SessionMessagingKit/Sending & Receiving/MessageReceiver+Handling.swift +++ b/SessionMessagingKit/Sending & Receiving/MessageReceiver+Handling.swift @@ -827,12 +827,14 @@ extension MessageReceiver { // a new configuration message (otherwise the `contact` will be loaded direct from the database and the // `didApproveMe` value won't have been updated) DispatchQueue.global(qos: .background).async { - guard Storage.shared.getUser()?.name != nil, let configurationMessage = ConfigurationMessage.getCurrent() else { - return + Storage.write { transaction in + guard Storage.shared.getUser()?.name != nil, let configurationMessage = ConfigurationMessage.getCurrent(with: transaction) else { + return + } + + let destination: Message.Destination = Message.Destination.contact(publicKey: userPublicKey) + MessageSender.send(configurationMessage, to: destination, using: transaction).retainUntilComplete() } - - let destination: Message.Destination = Message.Destination.contact(publicKey: userPublicKey) - MessageSender.send(configurationMessage, to: destination, using: transaction).retainUntilComplete() } } diff --git a/SessionMessagingKit/Storage.swift b/SessionMessagingKit/Storage.swift index 127cce708..3165d5202 100644 --- a/SessionMessagingKit/Storage.swift +++ b/SessionMessagingKit/Storage.swift @@ -17,15 +17,18 @@ public protocol SessionMessagingKitStorageProtocol { func getUserKeyPair() -> ECKeyPair? func getUserED25519KeyPair() -> Box.KeyPair? func getUser() -> Contact? + func getUser(using transaction: YapDatabaseReadTransaction?) -> Contact? func getAllContacts() -> Set func getAllContacts(with transaction: YapDatabaseReadTransaction) -> Set // MARK: - Closed Groups func getUserClosedGroupPublicKeys() -> Set + func getUserClosedGroupPublicKeys(using transaction: YapDatabaseReadTransaction) -> Set func getZombieMembers(for groupPublicKey: String) -> Set func setZombieMembers(for groupPublicKey: String, to zombies: Set, using transaction: Any) func isClosedGroup(_ publicKey: String) -> Bool + func isClosedGroup(_ publicKey: String, using transaction: YapDatabaseReadTransaction) -> Bool // MARK: - Jobs