From 1214005c59a3cf84dd0a144f52e4cd60a83ddff2 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Thu, 24 Mar 2022 14:42:25 +1100 Subject: [PATCH 1/5] Updated the cachedEncodedPublicKey to be Atomic Added the Atomic wrapper for thread safe variables --- Session.xcodeproj/project.pbxproj | 4 ++ Session/Settings/NukeDataModal.swift | 4 +- .../Database/Storage+Shared.swift | 2 +- SessionMessagingKit/Utilities/General.swift | 6 +-- SessionUtilitiesKit/General/Atomic.swift | 44 +++++++++++++++++++ 5 files changed, 54 insertions(+), 6 deletions(-) create mode 100644 SessionUtilitiesKit/General/Atomic.swift diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index f4998ffdb..ea715c12b 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -787,6 +787,7 @@ FD88BAD927A7439C00BBC442 /* MessageRequestsCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD88BAD827A7439C00BBC442 /* MessageRequestsCell.swift */; }; FD88BADB27A750F200BBC442 /* MessageRequestsMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD88BADA27A750F200BBC442 /* MessageRequestsMigration.swift */; }; FDC4389E27BA2B8A00C60D73 /* SessionUtilitiesKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A679255388CC00C340D1 /* SessionUtilitiesKit.framework */; }; + FDFD645827EC1F4000808CA1 /* Atomic.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDFD645727EC1F4000808CA1 /* Atomic.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -1829,6 +1830,7 @@ FD88BAD827A7439C00BBC442 /* MessageRequestsCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageRequestsCell.swift; sourceTree = ""; }; FD88BADA27A750F200BBC442 /* MessageRequestsMigration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageRequestsMigration.swift; sourceTree = ""; }; FD9039443F7CB729CF71350E /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-SessionNotificationServiceExtension.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-GlobalDependencies-FrameworkAndExtensionDependencies-SessionNotificationServiceExtension.debug.xcconfig"; path = "Pods/Target Support Files/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-SessionNotificationServiceExtension/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-SessionNotificationServiceExtension.debug.xcconfig"; sourceTree = ""; }; + FDFD645727EC1F4000808CA1 /* Atomic.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Atomic.swift; sourceTree = ""; }; FEDBAE1B98C49BBE8C87F575 /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit.debug.xcconfig"; path = "Pods/Target Support Files/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit.debug.xcconfig"; sourceTree = ""; }; FF9BA33D021B115B1F5B4E46 /* Pods-SessionMessagingKit.app store release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SessionMessagingKit.app store release.xcconfig"; path = "Pods/Target Support Files/Pods-SessionMessagingKit/Pods-SessionMessagingKit.app store release.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ @@ -2339,6 +2341,7 @@ C33FDB8A255A581200E217F9 /* AppContext.h */, C33FDB85255A581100E217F9 /* AppContext.m */, C3C2A5D12553860800C340D1 /* Array+Utilities.swift */, + FDFD645727EC1F4000808CA1 /* Atomic.swift */, C33FDAA8255A57FF00E217F9 /* BuildConfiguration.swift */, B8F5F58225EC94A6003BF8D4 /* Collection+Subscripting.swift */, B8AE75A325A6C6A6001A84D2 /* Data+Trimming.swift */, @@ -4680,6 +4683,7 @@ C32C5DDB256DD9FF003C73A2 /* ContentProxy.swift in Sources */, C3A71F892558BA9F0043A11F /* Mnemonic.swift in Sources */, B8F5F58325EC94A6003BF8D4 /* Collection+Subscripting.swift in Sources */, + FDFD645827EC1F4000808CA1 /* Atomic.swift in Sources */, C33FDEF8255A656D00E217F9 /* Promise+Delaying.swift in Sources */, B8BC00C0257D90E30032E807 /* General.swift in Sources */, C32C5A24256DB7DB003C73A2 /* SNUserDefaults.swift in Sources */, diff --git a/Session/Settings/NukeDataModal.swift b/Session/Settings/NukeDataModal.swift index e3110969d..dba878a22 100644 --- a/Session/Settings/NukeDataModal.swift +++ b/Session/Settings/NukeDataModal.swift @@ -128,7 +128,7 @@ final class NukeDataModal : Modal { appDelegate.forceSyncConfigurationNowIfNeeded().ensure(on: DispatchQueue.main) { self?.dismiss(animated: true, completion: nil) // Dismiss the loader UserDefaults.removeAll() // Not done in the nuke data implementation as unlinking requires this to happen later - General.Cache.cachedEncodedPublicKey = nil // Remove the cached key so it gets re-cached on next access + General.Cache.cachedEncodedPublicKey.mutate { $0 = nil } // Remove the cached key so it gets re-cached on next access NotificationCenter.default.post(name: .dataNukeRequested, object: nil) }.retainUntilComplete() } @@ -140,7 +140,7 @@ final class NukeDataModal : Modal { self?.dismiss(animated: true, completion: nil) // Dismiss the loader let potentiallyMaliciousSnodes = confirmations.compactMap { $0.value == false ? $0.key : nil } if potentiallyMaliciousSnodes.isEmpty { - General.Cache.cachedEncodedPublicKey = nil // Remove the cached key so it gets re-cached on next access + General.Cache.cachedEncodedPublicKey.mutate { $0 = nil } // Remove the cached key so it gets re-cached on next access UserDefaults.removeAll() // Not done in the nuke data implementation as unlinking requires this to happen later NotificationCenter.default.post(name: .dataNukeRequested, object: nil) } else { diff --git a/SessionMessagingKit/Database/Storage+Shared.swift b/SessionMessagingKit/Database/Storage+Shared.swift index 201a34f6f..d8f5de95d 100644 --- a/SessionMessagingKit/Database/Storage+Shared.swift +++ b/SessionMessagingKit/Database/Storage+Shared.swift @@ -40,7 +40,7 @@ extension Storage { } public func getUser(using transaction: YapDatabaseReadTransaction?) -> Contact? { - guard let userPublicKey = getUserPublicKey() else { return nil } + let userPublicKey = getUserHexEncodedPublicKey() var result: Contact? if let transaction = transaction { diff --git a/SessionMessagingKit/Utilities/General.swift b/SessionMessagingKit/Utilities/General.swift index 565338f65..d2e4e0c96 100644 --- a/SessionMessagingKit/Utilities/General.swift +++ b/SessionMessagingKit/Utilities/General.swift @@ -2,7 +2,7 @@ import Foundation public enum General { public enum Cache { - public static var cachedEncodedPublicKey: String? = nil + public static var cachedEncodedPublicKey: Atomic = Atomic(nil) } } @@ -14,10 +14,10 @@ public class GeneralUtilities: NSObject { } public func getUserHexEncodedPublicKey() -> String { - if let cachedKey: String = General.Cache.cachedEncodedPublicKey { return cachedKey } + if let cachedKey: String = General.Cache.cachedEncodedPublicKey.wrappedValue { return cachedKey } if let keyPair = OWSIdentityManager.shared().identityKeyPair() { // Can be nil under some circumstances - General.Cache.cachedEncodedPublicKey = keyPair.hexEncodedPublicKey + General.Cache.cachedEncodedPublicKey.mutate { $0 = keyPair.hexEncodedPublicKey } return keyPair.hexEncodedPublicKey } diff --git a/SessionUtilitiesKit/General/Atomic.swift b/SessionUtilitiesKit/General/Atomic.swift new file mode 100644 index 000000000..14baeed68 --- /dev/null +++ b/SessionUtilitiesKit/General/Atomic.swift @@ -0,0 +1,44 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. +import Foundation + +// MARK: - Atomic +/// The `Atomic` wrapper is a generic wrapper providing a thread-safe way to get and set a value +/// +/// A write-up on the need for this class and it's approach can be found here: +/// https://www.vadimbulavin.com/swift-atomic-properties-with-property-wrappers/ +/// there is also another approach which can be taken but it requires separate types for collections and results in +/// a somewhat inconsistent interface between different `Atomic` wrappers +@propertyWrapper +public class Atomic { + private let queue: DispatchQueue = DispatchQueue(label: "io.oxen.\(UUID().uuidString)") + private var value: Value + + /// In order to change the value you **must** use the `mutate` function + public var wrappedValue: Value { + return queue.sync { return value } + } + + /// For more information see https://github.com/apple/swift-evolution/blob/master/proposals/0258-property-wrappers.md#projections + public var projectedValue: Atomic { + return self + } + + // MARK: - Initialization + public init(_ initialValue: Value) { + self.value = initialValue + } + + // MARK: - Functions + + public func mutate(_ mutation: (inout Value) -> Void) { + return queue.sync { + mutation(&value) + } + } +} + +extension Atomic where Value: CustomDebugStringConvertible { + var debugDescription: String { + return value.debugDescription + } +} From 29c53223e00fc707df00631ba94a854678bdea59 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Thu, 24 Mar 2022 15:25:47 +1100 Subject: [PATCH 2/5] More tweaks to fix crash Wrapped the force sync calls within their own Storage.write blocks to ensure they have the latest data and aren't accessing a transaction completed in a different thread Reverted a number of the unneeded changes --- .../ConversationVC+Interaction.swift | 2 +- Session/Meta/AppDelegate.swift | 13 ++++---- .../Database/Storage+ClosedGroups.swift | 30 ++++--------------- .../Database/Storage+Shared.swift | 11 +------ .../ConfigurationMessage+Convenience.swift | 20 +++---------- SessionMessagingKit/Storage.swift | 3 -- 6 files changed, 20 insertions(+), 59 deletions(-) diff --git a/Session/Conversations/ConversationVC+Interaction.swift b/Session/Conversations/ConversationVC+Interaction.swift index 7e3aec685..21f0dfbee 100644 --- a/Session/Conversations/ConversationVC+Interaction.swift +++ b/Session/Conversations/ConversationVC+Interaction.swift @@ -1183,7 +1183,7 @@ extension ConversationVC { // Send a sync message with the details of the contact if let appDelegate = UIApplication.shared.delegate as? AppDelegate { - appDelegate.forceSyncConfigurationNowIfNeeded(with: transaction).retainUntilComplete() + appDelegate.forceSyncConfigurationNowIfNeeded().retainUntilComplete() } } } diff --git a/Session/Meta/AppDelegate.swift b/Session/Meta/AppDelegate.swift index 30525183d..d8064f1f6 100644 --- a/Session/Meta/AppDelegate.swift +++ b/Session/Meta/AppDelegate.swift @@ -22,14 +22,17 @@ extension AppDelegate { } } - func forceSyncConfigurationNowIfNeeded(with transaction: YapDatabaseReadWriteTransaction? = nil) -> Promise { - guard Storage.shared.getUser()?.name != nil, let configurationMessage = ConfigurationMessage.getCurrent(with: transaction) else { - return Promise.value(()) - } - + func forceSyncConfigurationNowIfNeeded() -> Promise { let destination = Message.Destination.contact(publicKey: getUserHexEncodedPublicKey()) let (promise, seal) = Promise.pending() + + // 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 { + seal.fulfill(()) + return + } + MessageSender.send(configurationMessage, to: destination, using: transaction).done { seal.fulfill(()) }.catch { _ in diff --git a/SessionMessagingKit/Database/Storage+ClosedGroups.swift b/SessionMessagingKit/Database/Storage+ClosedGroups.swift index a21d6d551..95f4f5a4b 100644 --- a/SessionMessagingKit/Database/Storage+ClosedGroups.swift +++ b/SessionMessagingKit/Database/Storage+ClosedGroups.swift @@ -10,19 +10,13 @@ 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)] = [] - transaction.enumerateKeysAndObjects(inCollection: collection) { key, object, _ in - guard let timestamp = Double(key), let keyPair = object as? ECKeyPair else { return } - timestampsAndKeyPairs.append((timestamp, keyPair)) + 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)) + } } return timestampsAndKeyPairs.sorted { $0.timestamp < $1.timestamp }.map { $0.keyPair } } @@ -30,10 +24,6 @@ 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) @@ -49,15 +39,11 @@ extension Storage { public func getUserClosedGroupPublicKeys() -> Set { var result: Set = [] Storage.read { transaction in - result = self.getUserClosedGroupPublicKeys(using: transaction) + result = Set(transaction.allKeys(inCollection: Storage.closedGroupPublicKeyCollection)) } 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) } @@ -95,8 +81,4 @@ 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 d8f5de95d..a975c2b76 100644 --- a/SessionMessagingKit/Database/Storage+Shared.swift +++ b/SessionMessagingKit/Database/Storage+Shared.swift @@ -36,21 +36,12 @@ extension Storage { } @objc public func getUser() -> Contact? { - return getUser(using: nil) - } - - public func getUser(using transaction: YapDatabaseReadTransaction?) -> Contact? { let userPublicKey = getUserHexEncodedPublicKey() var result: Contact? - if let transaction = transaction { + Storage.read { transaction in 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 641a055ae..2a172ecfb 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(with transaction: YapDatabaseReadWriteTransaction? = nil) -> ConfigurationMessage? { + public static func getCurrent() -> ConfigurationMessage? { let storage = Storage.shared - guard let user = storage.getUser(using: transaction) else { return nil } + guard let user = storage.getUser() else { return nil } let displayName = user.name let profilePictureURL = user.profilePictureURL @@ -13,7 +13,7 @@ extension ConfigurationMessage { var openGroups: Set = [] var contacts: Set = [] - let populateDataClosure: (YapDatabaseReadTransaction) -> () = { transaction in + Storage.read { transaction in TSGroupThread.enumerateCollectionObjects(with: transaction) { object, _ in guard let thread = object as? TSGroupThread else { return } @@ -24,10 +24,7 @@ extension ConfigurationMessage { let groupID = thread.groupModel.groupId let groupPublicKey = LKGroupUtilities.getDecodedGroupID(groupID) - guard - storage.isClosedGroup(groupPublicKey, using: transaction), - let encryptionKeyPair = storage.getLatestClosedGroupEncryptionKeyPair(for: groupPublicKey, using: transaction) - else { + guard storage.isClosedGroup(groupPublicKey), let encryptionKeyPair = storage.getLatestClosedGroupEncryptionKeyPair(for: groupPublicKey) else { return } @@ -91,15 +88,6 @@ extension ConfigurationMessage { ) } .asSet() - } - - // If we are provided with a transaction then read the data based on the state of the database - // from within the transaction rather than the state in disk - if let transaction: YapDatabaseReadWriteTransaction = transaction { - populateDataClosure(transaction) - } - else { - Storage.read { transaction in populateDataClosure(transaction) } } return ConfigurationMessage( diff --git a/SessionMessagingKit/Storage.swift b/SessionMessagingKit/Storage.swift index 3165d5202..127cce708 100644 --- a/SessionMessagingKit/Storage.swift +++ b/SessionMessagingKit/Storage.swift @@ -17,18 +17,15 @@ 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 From 212c5e87aa6401f8f924dd62e752a052a5ed5e3b Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Thu, 24 Mar 2022 15:34:46 +1100 Subject: [PATCH 3/5] Re-added the transaction requirement when generating the current config message --- Session/Meta/AppDelegate.swift | 9 +- .../Database/Storage+ClosedGroups.swift | 30 +++- .../Database/Storage+Shared.swift | 11 +- .../ConfigurationMessage+Convenience.swift | 153 +++++++++--------- .../MessageReceiver+Handling.swift | 12 +- SessionMessagingKit/Storage.swift | 3 + 6 files changed, 126 insertions(+), 92 deletions(-) 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 - } + 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, 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) + ) + 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() } + 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 From e4def22472e9f9c9bda2a25a0a73d0f71c145095 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Thu, 24 Mar 2022 15:46:53 +1100 Subject: [PATCH 4/5] Moved the Storage.write call into the `self.approveMessageRequestIfNeeded` call --- .../ConversationVC+Interaction.swift | 203 +++++++++--------- Session/Meta/AppDelegate.swift | 2 +- 2 files changed, 104 insertions(+), 101 deletions(-) diff --git a/Session/Conversations/ConversationVC+Interaction.swift b/Session/Conversations/ConversationVC+Interaction.swift index 21f0dfbee..0b26218e7 100644 --- a/Session/Conversations/ConversationVC+Interaction.swift +++ b/Session/Conversations/ConversationVC+Interaction.swift @@ -263,49 +263,46 @@ extension ConversationVC : InputViewDelegate, MessageCellDelegate, ContextMenuAc let linkPreviewDraft = snInputView.linkPreviewInfo?.draft let tsMessage = TSOutgoingMessage.from(message, associatedWith: thread) - Storage.write(with: { transaction in - let promise: Promise = self.approveMessageRequestIfNeeded( - for: self.thread, - with: transaction, - isNewThread: !oldThreadShouldBeVisible, - timestamp: (sentTimestamp - 1) // Set 1ms earlier as this is used for sorting - ) - .map { [weak self] _ in - self?.viewModel.appendUnsavedOutgoingTextMessage(tsMessage) - - Storage.write(with: { transaction in - message.linkPreview = VisibleMessage.LinkPreview.from(linkPreviewDraft, using: transaction) - }, completion: { [weak self] in - tsMessage.linkPreview = OWSLinkPreview.from(message.linkPreview) - - Storage.shared.write( - with: { transaction in - tsMessage.save(with: transaction as! YapDatabaseReadWriteTransaction) - }, - completion: { [weak self] in - // At this point the TSOutgoingMessage should have its link preview set, so we can scroll to the bottom knowing - // the height of the new message cell - self?.scrollToBottom(isAnimated: false) - } - ) - - Storage.shared.write { transaction in - MessageSender.send(message, with: [], in: thread, using: transaction as! YapDatabaseReadWriteTransaction) + let promise: Promise = self.approveMessageRequestIfNeeded( + for: self.thread, + isNewThread: !oldThreadShouldBeVisible, + timestamp: (sentTimestamp - 1) // Set 1ms earlier as this is used for sorting + ) + .map { [weak self] _ in + self?.viewModel.appendUnsavedOutgoingTextMessage(tsMessage) + + Storage.write(with: { transaction in + message.linkPreview = VisibleMessage.LinkPreview.from(linkPreviewDraft, using: transaction) + }, completion: { [weak self] in + tsMessage.linkPreview = OWSLinkPreview.from(message.linkPreview) + + Storage.shared.write( + with: { transaction in + tsMessage.save(with: transaction as! YapDatabaseReadWriteTransaction) + }, + completion: { [weak self] in + // At this point the TSOutgoingMessage should have its link preview set, so we can scroll to the bottom knowing + // the height of the new message cell + self?.scrollToBottom(isAnimated: false) } - - self?.handleMessageSent() - }) - } - - // Show an error indicating that approving the thread failed - promise.catch(on: DispatchQueue.main) { [weak self] _ in - let alert = UIAlertController(title: "Session", message: "An error occurred when trying to accept this message request", preferredStyle: .alert) - alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil)) - self?.present(alert, animated: true, completion: nil) - } - - promise.retainUntilComplete() - }) + ) + + Storage.shared.write { transaction in + MessageSender.send(message, with: [], in: thread, using: transaction as! YapDatabaseReadWriteTransaction) + } + + self?.handleMessageSent() + }) + } + + // Show an error indicating that approving the thread failed + promise.catch(on: DispatchQueue.main) { [weak self] _ in + let alert = UIAlertController(title: "Session", message: "An error occurred when trying to accept this message request", preferredStyle: .alert) + alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil)) + self?.present(alert, animated: true, completion: nil) + } + + promise.retainUntilComplete() } func sendAttachments(_ attachments: [SignalAttachment], with text: String, onComplete: (() -> ())? = nil) { @@ -329,44 +326,41 @@ extension ConversationVC : InputViewDelegate, MessageCellDelegate, ContextMenuAc let oldThreadShouldBeVisible: Bool = thread.shouldBeVisible let tsMessage = TSOutgoingMessage.from(message, associatedWith: thread) - Storage.write(with: { transaction in - let promise: Promise = self.approveMessageRequestIfNeeded( - for: self.thread, - with: transaction, - isNewThread: !oldThreadShouldBeVisible, - timestamp: (sentTimestamp - 1) // Set 1ms earlier as this is used for sorting - ) - .map { [weak self] _ in - Storage.write( - with: { transaction in - tsMessage.save(with: transaction) - // The new message cell is inserted at this point, but the TSOutgoingMessage doesn't have its attachment yet - }, - completion: { [weak self] in - Storage.write(with: { transaction in - MessageSender.send(message, with: attachments, in: thread, using: transaction) - }, completion: { [weak self] in - // At this point the TSOutgoingMessage should have its attachments set, so we can scroll to the bottom knowing - // the height of the new message cell - self?.scrollToBottom(isAnimated: false) - }) - self?.handleMessageSent() - - // Attachment successfully sent - dismiss the screen - onComplete?() - } - ) - } - - // Show an error indicating that approving the thread failed - promise.catch(on: DispatchQueue.main) { [weak self] _ in - let alert = UIAlertController(title: "Session", message: "An error occurred when trying to accept this message request", preferredStyle: .alert) - alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil)) - self?.present(alert, animated: true, completion: nil) - } + let promise: Promise = self.approveMessageRequestIfNeeded( + for: self.thread, + isNewThread: !oldThreadShouldBeVisible, + timestamp: (sentTimestamp - 1) // Set 1ms earlier as this is used for sorting + ) + .map { [weak self] _ in + Storage.write( + with: { transaction in + tsMessage.save(with: transaction) + // The new message cell is inserted at this point, but the TSOutgoingMessage doesn't have its attachment yet + }, + completion: { [weak self] in + Storage.write(with: { transaction in + MessageSender.send(message, with: attachments, in: thread, using: transaction) + }, completion: { [weak self] in + // At this point the TSOutgoingMessage should have its attachments set, so we can scroll to the bottom knowing + // the height of the new message cell + self?.scrollToBottom(isAnimated: false) + }) + self?.handleMessageSent() - promise.retainUntilComplete() - }) + // Attachment successfully sent - dismiss the screen + onComplete?() + } + ) + } + + // Show an error indicating that approving the thread failed + promise.catch(on: DispatchQueue.main) { [weak self] _ in + let alert = UIAlertController(title: "Session", message: "An error occurred when trying to accept this message request", preferredStyle: .alert) + alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil)) + self?.present(alert, animated: true, completion: nil) + } + + promise.retainUntilComplete() } func handleMessageSent() { @@ -1103,7 +1097,7 @@ extension ConversationVC: UIDocumentInteractionControllerDelegate { extension ConversationVC { - fileprivate func approveMessageRequestIfNeeded(for thread: TSThread?, with transaction: YapDatabaseReadWriteTransaction, isNewThread: Bool, timestamp: UInt64) -> Promise { + fileprivate func approveMessageRequestIfNeeded(for thread: TSThread?, isNewThread: Bool, timestamp: UInt64) -> Promise { guard let contactThread: TSContactThread = thread as? TSContactThread else { return Promise.value(()) } // If the contact doesn't exist then we should create it so we can store the 'isApproved' state @@ -1134,7 +1128,17 @@ extension ConversationVC { } return promise - .then { MessageSender.sendNonDurably(messageRequestResponse, in: contactThread, using: transaction) } + .then { _ -> Promise in + let (promise, seal) = Promise.pending() + Storage.writeSync { transaction in + MessageSender.sendNonDurably(messageRequestResponse, in: contactThread, using: transaction) + .done { seal.fulfill(()) } + .catch { _ in seal.fulfill(()) } // Fulfill even if this failed; the configuration in the swarm should be at most 2 days old + .retainUntilComplete() + } + + return promise + } .map { _ in if self?.presentedViewController is ModalActivityIndicatorViewController { self?.dismiss(animated: true, completion: nil) // Dismiss the loader @@ -1143,9 +1147,11 @@ extension ConversationVC { } .map { _ in // Default 'didApproveMe' to true for the person approving the message request - contact.isApproved = true - contact.didApproveMe = (contact.didApproveMe || !isNewThread) - Storage.shared.setContact(contact, using: transaction) + Storage.write { transaction in + contact.isApproved = true + contact.didApproveMe = (contact.didApproveMe || !isNewThread) + Storage.shared.setContact(contact, using: transaction) + } // Hide the 'messageRequestView' since the request has been approved and force a config // sync to propagate the contact approval state (both must run on the main thread) @@ -1190,23 +1196,20 @@ extension ConversationVC { } @objc func acceptMessageRequest() { - Storage.write { transaction in - let promise: Promise = self.approveMessageRequestIfNeeded( - for: self.thread, - with: transaction, - isNewThread: false, - timestamp: NSDate.millisecondTimestamp() - ) - - // Show an error indicating that approving the thread failed - promise.catch(on: DispatchQueue.main) { [weak self] _ in - let alert = UIAlertController(title: "Session", message: NSLocalizedString("MESSAGE_REQUESTS_APPROVAL_ERROR_MESSAGE", comment: ""), preferredStyle: .alert) - alert.addAction(UIAlertAction(title: NSLocalizedString("BUTTON_OK", comment: ""), style: .default, handler: nil)) - self?.present(alert, animated: true, completion: nil) - } - - promise.retainUntilComplete() + let promise: Promise = self.approveMessageRequestIfNeeded( + for: self.thread, + isNewThread: false, + timestamp: NSDate.millisecondTimestamp() + ) + + // Show an error indicating that approving the thread failed + promise.catch(on: DispatchQueue.main) { [weak self] _ in + let alert = UIAlertController(title: "Session", message: NSLocalizedString("MESSAGE_REQUESTS_APPROVAL_ERROR_MESSAGE", comment: ""), preferredStyle: .alert) + alert.addAction(UIAlertAction(title: NSLocalizedString("BUTTON_OK", comment: ""), style: .default, handler: nil)) + self?.present(alert, animated: true, completion: nil) } + + promise.retainUntilComplete() } @objc func deleteMessageRequest() { diff --git a/Session/Meta/AppDelegate.swift b/Session/Meta/AppDelegate.swift index a6815f954..6f1182807 100644 --- a/Session/Meta/AppDelegate.swift +++ b/Session/Meta/AppDelegate.swift @@ -29,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(with: transaction) else { + guard Storage.shared.getUser(using: transaction)?.name != nil, let configurationMessage = ConfigurationMessage.getCurrent(with: transaction) else { seal.fulfill(()) return } From 3663e63bc7c21069bc9cd0cd1efef2502f4b2eb9 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Thu, 24 Mar 2022 16:27:56 +1100 Subject: [PATCH 5/5] Swapped the Config message 'filter' to a 'compactMap' because apparently that doesn't crash --- .../ConfigurationMessage+Convenience.swift | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/SessionMessagingKit/Messages/Control Messages/ConfigurationMessage+Convenience.swift b/SessionMessagingKit/Messages/Control Messages/ConfigurationMessage+Convenience.swift index 386c17ae7..da328bd28 100644 --- a/SessionMessagingKit/Messages/Control Messages/ConfigurationMessage+Convenience.swift +++ b/SessionMessagingKit/Messages/Control Messages/ConfigurationMessage+Convenience.swift @@ -52,10 +52,10 @@ extension ConfigurationMessage { let currentUserPublicKey: String = getUserHexEncodedPublicKey() contacts = storage.getAllContacts(with: transaction) - .filter { contact -> Bool in + .compactMap { contact -> ConfigurationMessage.Contact? in let threadID = TSContactThread.threadID(fromContactSessionID: contact.sessionID) - return ( + guard // Skip the current user contact.sessionID != currentUserPublicKey && // Contacts which have visible threads @@ -68,9 +68,10 @@ extension ConfigurationMessage { // Sync blocked contacts SSKEnvironment.shared.blockingManager.isRecipientIdBlocked(contact.sessionID) ) - ) - } - .map { contact -> ConfigurationMessage.Contact in + else { + return nil + } + // Can just default the 'hasX' values to true as they will be set to this // when converting to proto anyway let profilePictureURL = contact.profilePictureURL