diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m index 2cd43c5b5..db19b3c78 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m +++ b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m @@ -4466,14 +4466,14 @@ typedef enum : NSUInteger { - (void)acceptFriendRequest:(TSIncomingMessage *)friendRequest { [OWSPrimaryStorage.sharedManager.dbReadWriteConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { - [LKFriendRequestProtocol acceptFriendRequestFrom:friendRequest.authorId in:self.thread using:transaction]; + [LKFriendRequestProtocol acceptFriendRequestInThread:self.thread using:transaction]; }]; } - (void)declineFriendRequest:(TSIncomingMessage *)friendRequest { [OWSPrimaryStorage.sharedManager.dbReadWriteConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { - [LKFriendRequestProtocol declineFriendRequest:friendRequest in:self.thread using:transaction]; + [LKFriendRequestProtocol declineFriendRequestInThread:self.thread using:transaction]; }]; } diff --git a/SignalServiceKit/src/Loki/Protocol/Friend Requests/FriendRequestProtocol.swift b/SignalServiceKit/src/Loki/Protocol/Friend Requests/FriendRequestProtocol.swift index 59eabc5a5..d6c25ab0d 100644 --- a/SignalServiceKit/src/Loki/Protocol/Friend Requests/FriendRequestProtocol.swift +++ b/SignalServiceKit/src/Loki/Protocol/Friend Requests/FriendRequestProtocol.swift @@ -15,6 +15,11 @@ public final class FriendRequestProtocol : NSObject { internal static var storage: OWSPrimaryStorage { OWSPrimaryStorage.shared() } + // Mark: - Status + private static func isPendingFriendRequest(_ status: LKFriendRequestStatus) -> Bool { + return status == .requestSending || status == .requestSent || status == .requestReceived + } + // MARK: - General @objc(shouldInputBarBeEnabledForThread:) public static func shouldInputBarBeEnabled(for thread: TSThread) -> Bool { @@ -23,14 +28,17 @@ public final class FriendRequestProtocol : NSObject { // If this is a note to self, the input bar should be enabled if SessionProtocol.isMessageNoteToSelf(thread) { return true } let contactID = thread.contactIdentifier() - var linkedDeviceThreads: Set = [] + var friendRequestStatuses: [LKFriendRequestStatus] = [] storage.dbReadConnection.read { transaction in - linkedDeviceThreads = LokiDatabaseUtilities.getLinkedDeviceThreads(for: contactID, in: transaction) + let linkedDeviceThreads = LokiDatabaseUtilities.getLinkedDeviceThreads(for: contactID, in: transaction) + friendRequestStatuses = linkedDeviceThreads.map { thread in + storage.getFriendRequestStatus(forContact: thread.contactIdentifier(), transaction: transaction) + } } // If the current user is friends with any of the other user's devices, the input bar should be enabled - if linkedDeviceThreads.contains(where: { $0.isContactFriend }) { return true } + if friendRequestStatuses.contains(where: { $0 == .friends }) { return true } // If no friend request has been sent, the input bar should be enabled - if !linkedDeviceThreads.contains(where: { $0.hasPendingFriendRequest }) { return true } + if !friendRequestStatuses.contains(where: { isPendingFriendRequest($0) }) { return true } // There must be a pending friend request return false } @@ -42,30 +50,38 @@ public final class FriendRequestProtocol : NSObject { // If this is a note to self, the attachment button should be enabled if SessionProtocol.isMessageNoteToSelf(thread) { return true } let contactID = thread.contactIdentifier() - var linkedDeviceThreads: Set = [] + var friendRequestStatuses: [LKFriendRequestStatus] = [] storage.dbReadConnection.read { transaction in - linkedDeviceThreads = LokiDatabaseUtilities.getLinkedDeviceThreads(for: contactID, in: transaction) + let linkedDeviceThreads = LokiDatabaseUtilities.getLinkedDeviceThreads(for: contactID, in: transaction) + friendRequestStatuses = linkedDeviceThreads.map { thread in + storage.getFriendRequestStatus(forContact: thread.contactIdentifier(), transaction: transaction) + } } // If the current user is friends with any of the other user's devices, the attachment button should be enabled - if linkedDeviceThreads.contains(where: { $0.isContactFriend }) { return true } - // If no friend request has been sent, the attachment button should be disabled - if !linkedDeviceThreads.contains(where: { $0.hasPendingFriendRequest }) { return false } - // There must be a pending friend request + if friendRequestStatuses.contains(where: { $0 == .friends }) { return true } + // Otherwise don't allow attachments at all return false } // MARK: - Sending - @objc(acceptFriendRequestFrom:in:using:) - public static func acceptFriendRequest(from hexEncodedPublicKey: String, in thread: TSThread, using transaction: YapDatabaseReadWriteTransaction) { + @objc(acceptFriendRequestInThread:using:) + public static func acceptFriendRequest(in thread: TSThread, using transaction: YapDatabaseReadWriteTransaction) { // Accept all outstanding friend requests associated with this user and try to establish sessions with the // subset of their devices that haven't sent a friend request. - let linkedDeviceThreads = LokiDatabaseUtilities.getLinkedDeviceThreads(for: hexEncodedPublicKey, in: transaction) // This doesn't create new threads if they don't exist yet - for thread in linkedDeviceThreads { - if thread.hasPendingFriendRequest { - sendFriendRequestAcceptanceMessage(to: thread.contactIdentifier(), in: thread, using: transaction) // NOT hexEncodedPublicKey - thread.saveFriendRequestStatus(.friends, with: transaction) - } else { - let autoGeneratedFRMessageSend = MultiDeviceProtocol.getAutoGeneratedMultiDeviceFRMessageSend(for: thread.contactIdentifier(), in: transaction) // NOT hexEncodedPublicKey + guard let thread = thread as? TSContactThread else { return } + + let linkedDevices = LokiDatabaseUtilities.getLinkedDeviceHexEncodedPublicKeys(for: thread.contactIdentifier(), in: transaction) + for device in linkedDevices { + let friendRequestStatus = storage.getFriendRequestStatus(forContact: device, transaction: transaction) + if friendRequestStatus == .requestReceived { + storage.setFriendRequestStatus(.friends, forContact: device, transaction: transaction) + // TODO: Do we need to pass in `thread` here? If not then we can restructure this whole function to take in a hex encoded public key instead + sendFriendRequestAcceptanceMessage(to: device, in: thread, using: transaction) + } else if friendRequestStatus == .requestSent { + // We sent a friend request to this device before, how can we be sure that it hasn't expired? + } else if friendRequestStatus == .none || friendRequestStatus == .requestExpired { + // TODO: Need to track these so that we can expire them and resend incase the other user wasn't online after we sent + let autoGeneratedFRMessageSend = MultiDeviceProtocol.getAutoGeneratedMultiDeviceFRMessageSend(for: device, in: transaction) OWSDispatch.sendingQueue().async { let messageSender = SSKEnvironment.shared.messageSender messageSender.sendMessage(autoGeneratedFRMessageSend) @@ -76,18 +92,28 @@ public final class FriendRequestProtocol : NSObject { @objc(sendFriendRequestAcceptanceMessageToHexEncodedPublicKey:in:using:) public static func sendFriendRequestAcceptanceMessage(to hexEncodedPublicKey: String, in thread: TSThread, using transaction: YapDatabaseReadWriteTransaction) { + guard let thread = thread as? TSContactThread else { return } + let ephemeralMessage = EphemeralMessage(in: thread) let messageSenderJobQueue = SSKEnvironment.shared.messageSenderJobQueue messageSenderJobQueue.add(message: ephemeralMessage, transaction: transaction) } - @objc(declineFriendRequest:in:using:) - public static func declineFriendRequest(_ friendRequest: TSIncomingMessage, in thread: TSThread, using transaction: YapDatabaseReadWriteTransaction) { - thread.saveFriendRequestStatus(.none, with: transaction) - // Delete the pre key bundle for the given contact. This ensures that if we send a - // new message after this, it restarts the friend request process from scratch. - let senderID = friendRequest.authorId - storage.removePreKeyBundle(forContact: senderID, transaction: transaction) + @objc(declineFriendRequestInThread:using:) + public static func declineFriendRequest(in thread: TSThread, using transaction: YapDatabaseReadWriteTransaction) { + guard let thread = thread as? TSContactThread else { return } + let linkedDevices = LokiDatabaseUtilities.getLinkedDeviceHexEncodedPublicKeys(for: thread.contactIdentifier(), in: transaction) + for device in linkedDevices { + let friendRequestStatus = storage.getFriendRequestStatus(forContact: device, transaction: transaction) + // We only want to decline any incoming requests + assert(friendRequestStatus != .friends, "Invalid state transition. Cannot decline a friend request from a device we're already friends with. Thread: \(thread.uniqueId) - \(thread.contactIdentifier())") + if (friendRequestStatus == .requestReceived) { + // Delete the pre key bundle for the given contact. This ensures that if we send a + // new message after this, it restarts the friend request process from scratch. + storage.removePreKeyBundle(forContact: device, transaction: transaction) + storage.setFriendRequestStatus(.none, forContact: device, transaction: transaction) + } + } } // MARK: - Receiving @@ -98,9 +124,9 @@ public final class FriendRequestProtocol : NSObject { return (envelope.type == .friendRequest && envelope.timestamp < restorationTimeInMs) } - @objc(canFriendRequestBeAutoAcceptedForHexEncodedPublicKey:in:using:) - public static func canFriendRequestBeAutoAccepted(for hexEncodedPublicKey: String, in thread: TSThread, using transaction: YapDatabaseReadTransaction) -> Bool { - if thread.hasCurrentUserSentFriendRequest { + @objc(canFriendRequestBeAutoAcceptedForHexEncodedPublicKey:using:) + public static func canFriendRequestBeAutoAccepted(for hexEncodedPublicKey: String, using transaction: YapDatabaseReadTransaction) -> Bool { + if storage.getFriendRequestStatus(forContact: hexEncodedPublicKey, transaction: transaction) == .requestSent { // This can happen if Alice sent Bob a friend request, Bob declined, but then Bob changed his // mind and sent a friend request to Alice. In this case we want Alice to auto-accept the request // and send a friend request accepted message back to Bob. We don't check that sending the @@ -117,8 +143,10 @@ public final class FriendRequestProtocol : NSObject { let userLinkedDeviceHexEncodedPublicKeys = LokiDatabaseUtilities.getLinkedDeviceHexEncodedPublicKeys(for: getUserHexEncodedPublicKey(), in: transaction) if userLinkedDeviceHexEncodedPublicKeys.contains(hexEncodedPublicKey) { return true } // Auto-accept if the user is friends with any of the sender's linked devices. - let senderLinkedDeviceThreads = LokiDatabaseUtilities.getLinkedDeviceThreads(for: hexEncodedPublicKey, in: transaction) - if senderLinkedDeviceThreads.contains(where: { $0.isContactFriend }) { return true } + let senderLinkedDevices = LokiDatabaseUtilities.getLinkedDeviceHexEncodedPublicKeys(for: hexEncodedPublicKey, in: transaction) + if senderLinkedDevices.contains(where: { storage.getFriendRequestStatus(forContact: $0, transaction: transaction) == .friends }) { + return true + } // We can't auto-accept return false } @@ -163,8 +191,8 @@ public final class FriendRequestProtocol : NSObject { print("[Loki] Ignoring friend request logic for non friend request type envelope.") return } - if canFriendRequestBeAutoAccepted(for: hexEncodedPublicKey, in: thread, using: transaction) { - thread.saveFriendRequestStatus(.friends, with: transaction) + if canFriendRequestBeAutoAccepted(for: hexEncodedPublicKey, using: transaction) { + storage.setFriendRequestStatus(.friends, forContact: hexEncodedPublicKey, transaction: transaction) var existingFriendRequestMessage: TSOutgoingMessage? thread.enumerateInteractions(with: transaction) { interaction, _ in if let outgoingMessage = interaction as? TSOutgoingMessage, outgoingMessage.isFriendRequest { @@ -175,13 +203,13 @@ public final class FriendRequestProtocol : NSObject { existingFriendRequestMessage.saveFriendRequestStatus(.accepted, with: transaction) } sendFriendRequestAcceptanceMessage(to: hexEncodedPublicKey, in: thread, using: transaction) - } else if !thread.isContactFriend { + } else if storage.getFriendRequestStatus(forContact: hexEncodedPublicKey, transaction: transaction) != .friends { // Checking that the sender of the message isn't already a friend is necessary because otherwise // the following situation can occur: Alice and Bob are friends. Bob loses his database and his // friend request status is reset to LKThreadFriendRequestStatusNone. Bob now sends Alice a friend // request. Alice's thread's friend request status is reset to // LKThreadFriendRequestStatusRequestReceived. - thread.saveFriendRequestStatus(.requestReceived, with: transaction) + storage.setFriendRequestStatus(.requestReceived, forContact: hexEncodedPublicKey, transaction: transaction) // Except for the message.friendRequestStatus = LKMessageFriendRequestStatusPending line below, all of this is to ensure that // there's only ever one message with status LKMessageFriendRequestStatusPending in a thread (where a thread is the combination // of all threads belonging to the linked devices of a user). diff --git a/SignalServiceKit/src/Loki/Protocol/Sync Messages/SyncMessagesProtocol.swift b/SignalServiceKit/src/Loki/Protocol/Sync Messages/SyncMessagesProtocol.swift index fd559ac35..5d105a0ce 100644 --- a/SignalServiceKit/src/Loki/Protocol/Sync Messages/SyncMessagesProtocol.swift +++ b/SignalServiceKit/src/Loki/Protocol/Sync Messages/SyncMessagesProtocol.swift @@ -196,7 +196,7 @@ public final class SyncMessagesProtocol : NSObject { case .requestReceived: storage.setFriendRequestStatus(.friends, forContact: hexEncodedPublicKey, transaction: transaction) // Not sendFriendRequestAcceptanceMessage(to:in:using:) to take into account multi device - FriendRequestProtocol.acceptFriendRequest(from: hexEncodedPublicKey, in: thread, using: transaction) + FriendRequestProtocol.acceptFriendRequest(in: thread, using: transaction) default: break } }