From 8a7db1d48feeb69dabac102e12bb6c5c4c7adb1b Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Wed, 2 Mar 2022 14:44:56 +1100 Subject: [PATCH] Started adding logic for the outbox endpoint Moved the BlindedIdMapping retrieval logic to ContactUtilities so it's reusable Added the 'outbox' endpoints (need testing as they aren't deployed to test yet) --- Session.xcodeproj/project.pbxproj | 8 +- Session/Closed Groups/EditClosedGroupVC.swift | 1 + Session/Closed Groups/NewClosedGroupVC.swift | 1 + .../ConversationVC+Interaction.swift | 67 ++----------- Session/Shared/UserSelectionVC.swift | 1 + Session/Utilities/ContactUtilities.swift | 51 ---------- .../Database/Storage+OpenGroups.swift | 23 +++++ .../Open Groups/Models/DirectMessage.swift | 8 +- .../Open Groups/OpenGroupAPI.swift | 61 ++++++++++-- .../Open Groups/OpenGroupManager.swift | 57 +++++++++-- .../Open Groups/Types/Dependencies.swift | 20 ++-- .../Open Groups/Types/Endpoint.swift | 10 +- .../Pollers/OpenGroupPoller.swift | 12 ++- SessionMessagingKit/Storage.swift | 16 +++ .../Utilities/ContactUtilities.swift | 99 +++++++++++++++++++ 15 files changed, 288 insertions(+), 147 deletions(-) delete mode 100644 Session/Utilities/ContactUtilities.swift create mode 100644 SessionMessagingKit/Utilities/ContactUtilities.swift diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index 64c422af3..3d58ff168 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -298,7 +298,6 @@ C31A6C5A247F214E001123EF /* UIView+Glow.swift in Sources */ = {isa = PBXBuildFile; fileRef = C31A6C59247F214E001123EF /* UIView+Glow.swift */; }; C31A6C5C247F2CF3001123EF /* CGRect+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = C31A6C5B247F2CF3001123EF /* CGRect+Utilities.swift */; }; C31D1DE32521718E005D4DA8 /* UserSelectionVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = C31D1DE22521718E005D4DA8 /* UserSelectionVC.swift */; }; - C31D1DE9252172D4005D4DA8 /* ContactUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = C31D1DE8252172D4005D4DA8 /* ContactUtilities.swift */; }; C31FFE57254A5FFE00F19441 /* KeyPairUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = C31FFE56254A5FFE00F19441 /* KeyPairUtilities.swift */; }; C32824D325C9F9790062D0A7 /* SessionSnodeKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A59F255385C100C340D1 /* SessionSnodeKit.framework */; }; C328250F25CA06020062D0A7 /* VoiceMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C328250E25CA06020062D0A7 /* VoiceMessageView.swift */; }; @@ -784,6 +783,7 @@ FD705A92278D051200F16121 /* ReusableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD705A91278D051200F16121 /* ReusableView.swift */; }; FD705A94278D052B00F16121 /* UITableView+ReusableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD705A93278D052B00F16121 /* UITableView+ReusableView.swift */; }; FD705A98278E9F4D00F16121 /* UIColor+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD705A97278E9F4D00F16121 /* UIColor+Extensions.swift */; }; + FD83B9AA27CF149D005E1583 /* ContactUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD83B9A927CF149D005E1583 /* ContactUtilities.swift */; }; FD859EF227BF6BA200510D0C /* Data+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD859EF127BF6BA200510D0C /* Data+Utilities.swift */; }; FD859EF427C2F49200510D0C /* TestSodium.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD859EF327C2F49200510D0C /* TestSodium.swift */; }; FD859EF627C2F52C00510D0C /* TestSign.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD859EF527C2F52C00510D0C /* TestSign.swift */; }; @@ -1410,7 +1410,6 @@ C31A6C5B247F2CF3001123EF /* CGRect+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CGRect+Utilities.swift"; sourceTree = ""; }; C31D1DDC25217014005D4DA8 /* UserCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserCell.swift; sourceTree = ""; }; C31D1DE22521718E005D4DA8 /* UserSelectionVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSelectionVC.swift; sourceTree = ""; }; - C31D1DE8252172D4005D4DA8 /* ContactUtilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactUtilities.swift; sourceTree = ""; }; C31FFE56254A5FFE00F19441 /* KeyPairUtilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyPairUtilities.swift; sourceTree = ""; }; C328250E25CA06020062D0A7 /* VoiceMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceMessageView.swift; sourceTree = ""; }; C328251E25CA3A900062D0A7 /* QuoteView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuoteView.swift; sourceTree = ""; }; @@ -1921,6 +1920,7 @@ FD705A91278D051200F16121 /* ReusableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReusableView.swift; sourceTree = ""; }; FD705A93278D052B00F16121 /* UITableView+ReusableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UITableView+ReusableView.swift"; sourceTree = ""; }; FD705A97278E9F4D00F16121 /* UIColor+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIColor+Extensions.swift"; sourceTree = ""; }; + FD83B9A927CF149D005E1583 /* ContactUtilities.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContactUtilities.swift; sourceTree = ""; }; FD859EEF27BF207700510D0C /* SessionProtos.proto */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.protobuf; path = SessionProtos.proto; sourceTree = ""; }; FD859EF027BF207C00510D0C /* WebSocketResources.proto */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.protobuf; path = WebSocketResources.proto; sourceTree = ""; }; FD859EF127BF6BA200510D0C /* Data+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Data+Utilities.swift"; sourceTree = ""; }; @@ -2229,7 +2229,6 @@ B8544E3223D50E4900299F14 /* SNAppearance.swift */, C3D0972A2510499C00F6E3E4 /* BackgroundPoller.swift */, C31A6C5B247F2CF3001123EF /* CGRect+Utilities.swift */, - C31D1DE8252172D4005D4DA8 /* ContactUtilities.swift */, C35E8AAD2485E51D00ACB629 /* IP2Country.swift */, C31FFE56254A5FFE00F19441 /* KeyPairUtilities.swift */, B84664F4235022F30083A1CD /* MentionUtilities.swift */, @@ -3395,6 +3394,7 @@ C33FDB01255A580700E217F9 /* AppReadiness.h */, C33FDB75255A581000E217F9 /* AppReadiness.m */, FDC4383D27B4708600C60D73 /* Atomic.swift */, + FD83B9A927CF149D005E1583 /* ContactUtilities.swift */, FD859EF127BF6BA200510D0C /* Data+Utilities.swift */, C38EF309255B6DBE007E1867 /* DeviceSleepManager.swift */, C37F53E8255BA9BB002AEA92 /* Environment.h */, @@ -5218,6 +5218,7 @@ FDC4382C27B380E300C60D73 /* LegacyMemberCountResponse.swift in Sources */, B8B32033258B235D0020074B /* Storage+Contacts.swift in Sources */, B8856D69256F141F001CE70E /* OWSWindowManager.m in Sources */, + FD83B9AA27CF149D005E1583 /* ContactUtilities.swift in Sources */, C3D9E3BE25676AD70040E4F3 /* TSAttachmentPointer.m in Sources */, C3ECBF7B257056B700EA7FCE /* Threading.swift in Sources */, C32C5AAB256DBE8F003C73A2 /* TSIncomingMessage+Conversion.swift in Sources */, @@ -5446,7 +5447,6 @@ C3AAFFF225AE99710089E6DD /* AppDelegate.swift in Sources */, B8BB82A5238F627000BA5194 /* HomeVC.swift in Sources */, C31A6C5A247F214E001123EF /* UIView+Glow.swift in Sources */, - C31D1DE9252172D4005D4DA8 /* ContactUtilities.swift in Sources */, 4521C3C01F59F3BA00B4C582 /* TextFieldHelper.swift in Sources */, 340FC8AC204DAC8D007AEB0F /* PrivacySettingsTableViewController.m in Sources */, B8569AE325CBB19A00DBA3DB /* DocumentView.swift in Sources */, diff --git a/Session/Closed Groups/EditClosedGroupVC.swift b/Session/Closed Groups/EditClosedGroupVC.swift index 3a74dea3e..e84b333b5 100644 --- a/Session/Closed Groups/EditClosedGroupVC.swift +++ b/Session/Closed Groups/EditClosedGroupVC.swift @@ -1,4 +1,5 @@ import PromiseKit +import SessionMessagingKit @objc(SNEditClosedGroupVC) final class EditClosedGroupVC : BaseVC, UITableViewDataSource, UITableViewDelegate { diff --git a/Session/Closed Groups/NewClosedGroupVC.swift b/Session/Closed Groups/NewClosedGroupVC.swift index 47fbbff43..57ddd3f12 100644 --- a/Session/Closed Groups/NewClosedGroupVC.swift +++ b/Session/Closed Groups/NewClosedGroupVC.swift @@ -1,4 +1,5 @@ import PromiseKit +import SessionMessagingKit private protocol TableViewTouchDelegate { diff --git a/Session/Conversations/ConversationVC+Interaction.swift b/Session/Conversations/ConversationVC+Interaction.swift index 432167228..fc5b3e072 100644 --- a/Session/Conversations/ConversationVC+Interaction.swift +++ b/Session/Conversations/ConversationVC+Interaction.swift @@ -4,6 +4,7 @@ import Photos import PhotosUI import Sodium import PromiseKit +import SessionMessagingKit import SessionUtilitiesKit import SignalUtilitiesKit @@ -866,68 +867,12 @@ extension ConversationVC : InputViewDelegate, MessageCellDelegate, ContextMenuAc func startThread(with sessionId: String, openGroupServer: String, openGroupPublicKey: String) { // If the sessionId is blinded then check if there is an existing un-blinded thread with the contact - if SessionId.Prefix(from: sessionId) == .blinded { - // TODO: Ensure the above case isn't going to be an issue due to legacy messages? - // Unfortunately the whole point of id-blinding is to make it hard to reverse-engineer a standard - // sessionId, as a result in order to see if there is an unblinded contact for this blindedId we - // can only really generate blinded ids for each contact and check if any match - // - // Due to this we have made a few optimisations to try and early-out as often as possible, first - // we try to retrieve a direct cached mapping - if let mapping: BlindedIdMapping = Storage.shared.getBlindedIdMapping(with: sessionId) { - let thread: TSContactThread = TSContactThread.getOrCreateThread(contactSessionID: mapping.sessionId) - let conversationVC: ConversationVC = ConversationVC(thread: thread) - - self.navigationController?.pushViewController(conversationVC, animated: true) - return - } - - var didFindContact: Bool = false - - // Then we try loop through all approved contact threads to see if one of those contacts can be blinded to match - ContactUtilities.enumerateApprovedContactThreads { contactThread, contact, stop in - guard Sodium().sessionId(contact.sessionID, matchesBlindedId: sessionId, serverPublicKey: openGroupPublicKey) else { - return - } - - // Cache the mapping - let mapping: BlindedIdMapping = BlindedIdMapping(blindedId: sessionId, sessionId: contact.sessionID, serverPublicKey: openGroupPublicKey) - Storage.shared.cacheBlindedIdMapping(mapping) - - // Open the existing thread - let conversationVC: ConversationVC = ConversationVC(thread: contactThread) - self.navigationController?.pushViewController(conversationVC, animated: true) - - didFindContact = true - stop.pointee = true - } - - // Don't continue if we found the contact - guard !didFindContact else { return } + if SessionId.Prefix(from: sessionId) == .blinded, let mapping: BlindedIdMapping = ContactUtilities.mapping(for: sessionId, serverPublicKey: openGroupPublicKey) { + let thread: TSContactThread = TSContactThread.getOrCreateThread(contactSessionID: mapping.sessionId) + let conversationVC: ConversationVC = ConversationVC(thread: thread) - // Lastly loop through existing id mappings (in case the user is looking at a different SOGS but once had - // a thread with this contact in a different SOGS and had cached the mapping) - Storage.shared.enumerateBlindedIdMapping { mapping, stop in - guard mapping.serverPublicKey != openGroupPublicKey else { return } - guard Sodium().sessionId(mapping.sessionId, matchesBlindedId: sessionId, serverPublicKey: openGroupPublicKey) else { - return - } - - // Cache the new mapping - let thread: TSContactThread = TSContactThread.getOrCreateThread(contactSessionID: mapping.sessionId) - let newMapping: BlindedIdMapping = BlindedIdMapping(blindedId: sessionId, sessionId: mapping.sessionId, serverPublicKey: openGroupPublicKey) - Storage.shared.cacheBlindedIdMapping(newMapping) - - // Open the existing thread - let conversationVC: ConversationVC = ConversationVC(thread: thread) - self.navigationController?.pushViewController(conversationVC, animated: true) - - didFindContact = true - stop.pointee = true - } - - // Don't continue if we found the contact - guard !didFindContact else { return } + self.navigationController?.pushViewController(conversationVC, animated: true) + return } // Just create a new thread with the provided sessionId diff --git a/Session/Shared/UserSelectionVC.swift b/Session/Shared/UserSelectionVC.swift index b62104da3..88ce0abb2 100644 --- a/Session/Shared/UserSelectionVC.swift +++ b/Session/Shared/UserSelectionVC.swift @@ -1,3 +1,4 @@ +import SessionMessagingKit @objc(SNUserSelectionVC) final class UserSelectionVC : BaseVC, UITableViewDataSource, UITableViewDelegate { diff --git a/Session/Utilities/ContactUtilities.swift b/Session/Utilities/ContactUtilities.swift deleted file mode 100644 index 6c7857374..000000000 --- a/Session/Utilities/ContactUtilities.swift +++ /dev/null @@ -1,51 +0,0 @@ - -enum ContactUtilities { - private static func approvedContact(in threadObject: Any, using transaction: Any) -> Contact? { - guard let thread: TSContactThread = threadObject as? TSContactThread else { return nil } - guard thread.shouldBeVisible else { return nil } - guard let contact: Contact = Storage.shared.getContact(with: thread.contactSessionID(), using: transaction) else { - return nil - } - guard contact.didApproveMe else { return nil } - - return contact - } - - static func getAllContacts() -> [String] { - // Collect all contacts - var result: [Contact] = [] - Storage.read { transaction in - TSContactThread.enumerateCollectionObjects(with: transaction) { object, _ in - guard let contact: Contact = approvedContact(in: object, using: transaction) else { return } - - result.append(contact) - } - } - func getDisplayName(for publicKey: String) -> String { - return Storage.shared.getContact(with: publicKey)?.displayName(for: .regular) ?? publicKey - } - - // Remove the current user - if let index = result.firstIndex(where: { $0.sessionID == getUserHexEncodedPublicKey() }) { - result.remove(at: index) - } - - // Sort alphabetically - return result - .sorted(by: { lhs, rhs in - (lhs.displayName(for: .regular) ?? lhs.sessionID) < (rhs.displayName(for: .regular) ?? rhs.sessionID) - }) - .map { $0.sessionID } - } - - static func enumerateApprovedContactThreads(with block: @escaping (TSContactThread, Contact, UnsafeMutablePointer) -> ()) { - Storage.read { transaction in - TSContactThread.enumerateCollectionObjects(with: transaction) { object, stop in - guard let contactThread: TSContactThread = object as? TSContactThread else { return } - guard let contact: Contact = approvedContact(in: object, using: transaction) else { return } - - block(contactThread, contact, stop) - } - } - } -} diff --git a/SessionMessagingKit/Database/Storage+OpenGroups.swift b/SessionMessagingKit/Database/Storage+OpenGroups.swift index 5031bfe69..f3b7cf933 100644 --- a/SessionMessagingKit/Database/Storage+OpenGroups.swift +++ b/SessionMessagingKit/Database/Storage+OpenGroups.swift @@ -131,6 +131,29 @@ extension Storage { let collection = Storage.openGroupInboxLatestMessageIdCollection (transaction as! YapDatabaseReadWriteTransaction).removeObject(forKey: server, inCollection: collection) } + + // MARK: - -- Open Group Outbox Latest Message Id + + public static let openGroupOutboxLatestMessageIdCollection = "SNOpenGroupOutboxLatestMessageIdCollection" + + public func getOpenGroupOutboxLatestMessageId(for server: String) -> Int64? { + let collection = Storage.openGroupOutboxLatestMessageIdCollection + var result: Int64? = nil + Storage.read { transaction in + result = transaction.object(forKey: server, inCollection: collection) as? Int64 + } + return result + } + + public func setOpenGroupOutboxLatestMessageId(for server: String, to newValue: Int64, using transaction: Any) { + let collection = Storage.openGroupOutboxLatestMessageIdCollection + (transaction as! YapDatabaseReadWriteTransaction).setObject(newValue, forKey: server, inCollection: collection) + } + + public func removeOpenGroupOutboxLatestMessageId(for server: String, using transaction: Any) { + let collection = Storage.openGroupOutboxLatestMessageIdCollection + (transaction as! YapDatabaseReadWriteTransaction).removeObject(forKey: server, inCollection: collection) + } // MARK: - Metadata diff --git a/SessionMessagingKit/Open Groups/Models/DirectMessage.swift b/SessionMessagingKit/Open Groups/Models/DirectMessage.swift index 8ea11aaf1..941f21b7e 100644 --- a/SessionMessagingKit/Open Groups/Models/DirectMessage.swift +++ b/SessionMessagingKit/Open Groups/Models/DirectMessage.swift @@ -7,6 +7,7 @@ extension OpenGroupAPI { enum CodingKeys: String, CodingKey { case id case sender + case recipient case posted = "posted_at" case expires = "expires_at" case base64EncodedMessage = "message" @@ -15,8 +16,11 @@ extension OpenGroupAPI { /// The unique integer message id public let id: Int64 - /// The (blinded) Session ID of the sender of the message - public let sender: String + /// The (blinded) Session ID of the sender of the message (null on outgoing messages) + public let sender: String? + + /// The (blinded) Session ID of the recipient of the message (null on incoming message) + public let recipient: String? /// Unix timestamp when the message was received by SOGS public let posted: TimeInterval diff --git a/SessionMessagingKit/Open Groups/OpenGroupAPI.swift b/SessionMessagingKit/Open Groups/OpenGroupAPI.swift index 35a90cbcf..ff06432da 100644 --- a/SessionMessagingKit/Open Groups/OpenGroupAPI.swift +++ b/SessionMessagingKit/Open Groups/OpenGroupAPI.swift @@ -41,12 +41,15 @@ public final class OpenGroupAPI: NSObject { /// - Poll Info /// - Messages (includes additions and deletions) /// - Inbox for the server + /// - Outbox for the server public static func poll(_ server: String, using dependencies: Dependencies = Dependencies()) -> Promise<[Endpoint: (OnionRequestResponseInfoType, Codable?)]> { // Store a local copy of the cached state for this server let hadPerformedInitialPoll: Bool = (hasPerformedInitialPoll.wrappedValue[server] == true) let originalTimeSinceLastPoll: TimeInterval = (timeSinceLastPoll.wrappedValue[server] ?? min(lastPollTime.wrappedValue, timeSinceLastOpen.wrappedValue)) let maybeLastInboxMessageId: Int64? = dependencies.storage.getOpenGroupInboxLatestMessageId(for: server) + let maybeLastOutboxMessageId: Int64? = dependencies.storage.getOpenGroupOutboxLatestMessageId(for: server) let lastInboxMessageId: Int64 = (maybeLastInboxMessageId ?? 0) + let lastOutboxMessageId: Int64 = (maybeLastOutboxMessageId ?? 0) // Update the cached state for this server hasPerformedInitialPoll.mutate { $0[server] = true } @@ -95,15 +98,13 @@ public final class OpenGroupAPI: NSObject { .roomMessagesRecent(openGroup.room) : .roomMessagesSince(openGroup.room, seqNo: targetSeqNo) ) - // TODO: Limit? -// queryParameters: [ .limit: 256 ] ), responseType: [Message].self ) ] } ) - .appending( + .appending([ // Inbox BatchRequestInfo( request: Request( @@ -112,12 +113,22 @@ public final class OpenGroupAPI: NSObject { .inbox : .inboxSince(id: lastInboxMessageId) ) - // TODO: Limit? -// queryParameters: [ .limit: 256 ] ), responseType: [DirectMessage]?.self // 'inboxSince' will return a `304` with an empty response if no messages + ), + + // Outbox + BatchRequestInfo( + request: Request( + server: server, + endpoint: (maybeLastOutboxMessageId == nil ? + .outbox : + .outboxSince(id: lastOutboxMessageId) + ) + ), + responseType: [DirectMessage]?.self // 'outboxSince' will return a `304` with an empty response if no messages ) - ) + ]) return batch(server, requests: requestResponseType, using: dependencies) } @@ -499,13 +510,13 @@ public final class OpenGroupAPI: NSObject { .decoded(as: FileDownloadResponse.self, on: OpenGroupAPI.workQueue, error: Error.parsingFailed, using: dependencies) } - // MARK: - Inbox (Message Requests) + // MARK: - Inbox/Outbox (Message Requests) /// Retrieves all of the user's current DMs (up to limit) /// /// **Note:** This is the direct request to retrieve DMs for a specific Open Group so should be retrieved automatically from the `poll()` /// method, in order to call this directly remove the `@available` line and make sure to route the response of this method to the - /// `OpenGroupManager.handleInbox` method to ensure things are processed correctly + /// `OpenGroupManager.handleDirectMessages` method to ensure things are processed correctly @available(*, unavailable, message: "Avoid using this directly, use the pre-build `poll()` method instead") public static func inbox(on server: String, using dependencies: Dependencies = Dependencies()) -> Promise<(OnionRequestResponseInfoType, [DirectMessage]?)> { let request: Request = Request( @@ -521,7 +532,7 @@ public final class OpenGroupAPI: NSObject { /// /// **Note:** This is the direct request to retrieve messages requests for a specific Open Group since a given messages so should be retrieved /// automatically from the `poll()` method, in order to call this directly remove the `@available` line and make sure to route the response - /// of this method to the `OpenGroupManager.handleInbox` method to ensure things are processed correctly + /// of this method to the `OpenGroupManager.handleDirectMessages` method to ensure things are processed correctly @available(*, unavailable, message: "Avoid using this directly, use the pre-build `poll()` method instead") public static func inboxSince(id: Int64, on server: String, using dependencies: Dependencies = Dependencies()) -> Promise<(OnionRequestResponseInfoType, [DirectMessage]?)> { let request: Request = Request( @@ -551,6 +562,38 @@ public final class OpenGroupAPI: NSObject { return send(request, using: dependencies) } + /// Retrieves all of the user's sent DMs (up to limit) + /// + /// **Note:** This is the direct request to retrieve DMs sent by the user for a specific Open Group so should be retrieved automatically + /// from the `poll()` method, in order to call this directly remove the `@available` line and make sure to route the response of + /// this method to the `OpenGroupManager.handleDirectMessages` method to ensure things are processed correctly + @available(*, unavailable, message: "Avoid using this directly, use the pre-build `poll()` method instead") + public static func outbox(on server: String, using dependencies: Dependencies = Dependencies()) -> Promise<(OnionRequestResponseInfoType, [DirectMessage]?)> { + let request: Request = Request( + server: server, + endpoint: .outbox + ) + + return send(request, using: dependencies) + .decoded(as: [DirectMessage]?.self, on: OpenGroupAPI.workQueue, error: Error.parsingFailed, using: dependencies) + } + + /// Polls for any DMs sent since the given id, this method will return a `304` with an empty response if there are no messages + /// + /// **Note:** This is the direct request to retrieve messages requests sent by the user for a specific Open Group since a given messages so + /// should be retrieved automatically from the `poll()` method, in order to call this directly remove the `@available` line and make sure + /// to route the response of this method to the `OpenGroupManager.handleDirectMessages` method to ensure things are processed correctly + @available(*, unavailable, message: "Avoid using this directly, use the pre-build `poll()` method instead") + public static func outboxSince(id: Int64, on server: String, using dependencies: Dependencies = Dependencies()) -> Promise<(OnionRequestResponseInfoType, [DirectMessage]?)> { + let request: Request = Request( + server: server, + endpoint: .outboxSince(id: id) + ) + + return send(request, using: dependencies) + .decoded(as: [DirectMessage]?.self, on: OpenGroupAPI.workQueue, error: Error.parsingFailed, using: dependencies) + } + // MARK: - Users /// Applies a ban of a user from specific rooms, or from the server globally diff --git a/SessionMessagingKit/Open Groups/OpenGroupManager.swift b/SessionMessagingKit/Open Groups/OpenGroupManager.swift index 37d5e339a..e80a03dd7 100644 --- a/SessionMessagingKit/Open Groups/OpenGroupManager.swift +++ b/SessionMessagingKit/Open Groups/OpenGroupManager.swift @@ -248,7 +248,7 @@ public final class OpenGroupManager: NSObject { maybeUpdatedModel = updatedModel let updatedOpenGroup: OpenGroup = OpenGroup( server: server, - room: (pollInfo.token ?? roomToken), + room: pollInfo.token, publicKey: publicKey, name: (pollInfo.details?.name ?? thread.name()), groupDescription: (pollInfo.details?.description ?? existingOpenGroup?.description), @@ -311,8 +311,11 @@ public final class OpenGroupManager: NSObject { ) } - internal static func handleInbox( + internal static func handleDirectMessages( _ messages: [OpenGroupAPI.DirectMessage], + // We could infer where the messages come from based on their sender/recipient values but being since they + // are different endpoints being explicit here reduces the chance a future change will break things + fromOutbox: Bool, on server: String, isBackgroundPoll: Bool, using dependencies: OpenGroupAPI.Dependencies = OpenGroupAPI.Dependencies() @@ -329,10 +332,17 @@ public final class OpenGroupManager: NSObject { let sortedMessages: [OpenGroupAPI.DirectMessage] = messages .sorted { lhs, rhs in lhs.id < rhs.id } let latestMessageId: Int64 = (sortedMessages.last?.id ?? 0) + let userSessionId: String = getUserHexEncodedPublicKey() + var mappingCache: [String: BlindedIdMapping] = [:] dependencies.storage.write { transaction in // Update the 'latestMessageId' value - dependencies.storage.setOpenGroupInboxLatestMessageId(for: server, to: latestMessageId, using: transaction) + if fromOutbox { + dependencies.storage.setOpenGroupOutboxLatestMessageId(for: server, to: latestMessageId, using: transaction) + } + else { + dependencies.storage.setOpenGroupInboxLatestMessageId(for: server, to: latestMessageId, using: transaction) + } // Process the messages sortedMessages.forEach { message in @@ -344,12 +354,47 @@ public final class OpenGroupManager: NSObject { // Note: The `posted` value is in seconds but all messages in the database use milliseconds for timestamps let envelope = SNProtoEnvelope.builder(type: .sessionMessage, timestamp: UInt64(floor(message.posted * 1000))) envelope.setContent(messageData) - envelope.setSource(message.sender) + envelope.setSource(message.sender ?? userSessionId) // Outbox messages have no 'sender' so default to current user do { let data = try envelope.buildSerializedData() - let (message, proto) = try MessageReceiver.parse(data, openGroupMessageServerID: nil, openGroupServerPublicKey: serverPublicKey, isRetry: false, using: transaction) - try MessageReceiver.handle(message, associatedWithProto: proto, openGroupID: nil, isBackgroundPoll: isBackgroundPoll, using: transaction) + let (receivedMessage, proto) = try MessageReceiver.parse(data, openGroupMessageServerID: nil, openGroupServerPublicKey: serverPublicKey, isRetry: false, using: transaction) + + // TODO: Need to test and validate this unblinding logic + // If the message was an outgoing message then attempt to unblind the recipient (this will help put + // messages in the correct thread in case of message request approval race conditions as well as + // during device sync'ing and restoration) + if fromOutbox, let recipientBlindedId: String = message.recipient { + // Attempt to un-blind the 'message.recipient' + let mapping: BlindedIdMapping + + // Minor optimisation to avoid processing the same sender multiple times + if let result: BlindedIdMapping = mappingCache[recipientBlindedId] { + mapping = result + } + else if let result: BlindedIdMapping = ContactUtilities.mapping(for: recipientBlindedId, serverPublicKey: serverPublicKey) { + mapping = result + } + else { + // Cache an "invalid" mapping that has the 'sessionId' set to the recipient so we don't + // re-process this recipient if there is another message from them + mapping = BlindedIdMapping( + blindedId: "", + sessionId: recipientBlindedId, + serverPublicKey: "" + ) + } + + switch receivedMessage { + case let receivedMessage as VisibleMessage: receivedMessage.syncTarget = mapping.sessionId + case let receivedMessage as ExpirationTimerUpdate: receivedMessage.syncTarget = mapping.sessionId + default: break + } + + mappingCache[recipientBlindedId] = mapping + } + + try MessageReceiver.handle(receivedMessage, associatedWithProto: proto, openGroupID: nil, isBackgroundPoll: isBackgroundPoll, using: transaction) } catch let error { SNLog("Couldn't receive inbox message due to error: \(error).") diff --git a/SessionMessagingKit/Open Groups/Types/Dependencies.swift b/SessionMessagingKit/Open Groups/Types/Dependencies.swift index fd64511b8..86479e06f 100644 --- a/SessionMessagingKit/Open Groups/Types/Dependencies.swift +++ b/SessionMessagingKit/Open Groups/Types/Dependencies.swift @@ -9,61 +9,61 @@ import SessionSnodeKit extension OpenGroupAPI { public class Dependencies { private var _api: OnionRequestAPIType.Type? - var api: OnionRequestAPIType.Type { + public var api: OnionRequestAPIType.Type { get { getValueSettingIfNull(&_api) { OnionRequestAPI.self } } set { _api = newValue } } private var _storage: SessionMessagingKitStorageProtocol? - var storage: SessionMessagingKitStorageProtocol { + public var storage: SessionMessagingKitStorageProtocol { get { getValueSettingIfNull(&_storage) { SNMessagingKitConfiguration.shared.storage } } set { _storage = newValue } } private var _sodium: SodiumType? - var sodium: SodiumType { + public var sodium: SodiumType { get { getValueSettingIfNull(&_sodium) { Sodium() } } set { _sodium = newValue } } private var _aeadXChaCha20Poly1305Ietf: AeadXChaCha20Poly1305IetfType? - var aeadXChaCha20Poly1305Ietf: AeadXChaCha20Poly1305IetfType { + public var aeadXChaCha20Poly1305Ietf: AeadXChaCha20Poly1305IetfType { get { getValueSettingIfNull(&_aeadXChaCha20Poly1305Ietf) { sodium.getAeadXChaCha20Poly1305Ietf() } } set { _aeadXChaCha20Poly1305Ietf = newValue } } private var _sign: SignType? - var sign: SignType { + public var sign: SignType { get { getValueSettingIfNull(&_sign) { sodium.getSign() } } set { _sign = newValue } } private var _genericHash: GenericHashType? - var genericHash: GenericHashType { + public var genericHash: GenericHashType { get { getValueSettingIfNull(&_genericHash) { sodium.getGenericHash() } } set { _genericHash = newValue } } private var _ed25519: Ed25519Type.Type? - var ed25519: Ed25519Type.Type { + public var ed25519: Ed25519Type.Type { get { getValueSettingIfNull(&_ed25519) { Ed25519.self } } set { _ed25519 = newValue } } private var _nonceGenerator16: NonceGenerator16ByteType? - var nonceGenerator16: NonceGenerator16ByteType { + public var nonceGenerator16: NonceGenerator16ByteType { get { getValueSettingIfNull(&_nonceGenerator16) { NonceGenerator16Byte() } } set { _nonceGenerator16 = newValue } } private var _nonceGenerator24: NonceGenerator24ByteType? - var nonceGenerator24: NonceGenerator24ByteType { + public var nonceGenerator24: NonceGenerator24ByteType { get { getValueSettingIfNull(&_nonceGenerator24) { NonceGenerator24Byte() } } set { _nonceGenerator24 = newValue } } private var _date: Date? - var date: Date { + public var date: Date { get { getValueSettingIfNull(&_date) { Date() } } set { _date = newValue } } diff --git a/SessionMessagingKit/Open Groups/Types/Endpoint.swift b/SessionMessagingKit/Open Groups/Types/Endpoint.swift index e52fa6375..9149aec5b 100644 --- a/SessionMessagingKit/Open Groups/Types/Endpoint.swift +++ b/SessionMessagingKit/Open Groups/Types/Endpoint.swift @@ -38,12 +38,15 @@ extension OpenGroupAPI { case roomFileIndividual(String, Int64) case roomFileIndividualJson(String, Int64) - // Inbox (Message Requests) + // Inbox/Outbox (Message Requests) case inbox case inboxSince(id: Int64) case inboxFor(sessionId: String) + case outbox + case outboxSince(id: Int64) + // Users case userBan(String) @@ -133,11 +136,14 @@ extension OpenGroupAPI { case .roomFileIndividualJson(let roomToken, let fileId): return "room/\(roomToken)/file/\(fileId)" - // Inbox (Message Requests) + // Inbox/Outbox (Message Requests) case .inbox: return "inbox" case .inboxSince(let id): return "inbox/since/\(id)" case .inboxFor(let sessionId): return "inbox/\(sessionId)" + + case .outbox: return "outbox" + case .outboxSince(let id): return "outbox/since/\(id)" // Users diff --git a/SessionMessagingKit/Sending & Receiving/Pollers/OpenGroupPoller.swift b/SessionMessagingKit/Sending & Receiving/Pollers/OpenGroupPoller.swift index 793200253..224e5ed15 100644 --- a/SessionMessagingKit/Sending & Receiving/Pollers/OpenGroupPoller.swift +++ b/SessionMessagingKit/Sending & Receiving/Pollers/OpenGroupPoller.swift @@ -123,14 +123,22 @@ extension OpenGroupAPI { on: server ) - case .inbox, .inboxSince: + case .inbox, .inboxSince, .outbox, .outboxSince: guard let responseData: BatchSubResponse<[DirectMessage]?> = endpointResponse.data as? BatchSubResponse<[DirectMessage]?>, let responseBody: [DirectMessage]? = responseData.body else { SNLog("Open group polling failed due to invalid data.") return } - OpenGroupManager.handleInbox( + let fromOutbox: Bool = { + switch endpoint { + case .outbox, .outboxSince: return true + default: return false + } + }() + + OpenGroupManager.handleDirectMessages( (responseBody ?? []), + fromOutbox: fromOutbox, on: server, isBackgroundPoll: isBackgroundPoll ) diff --git a/SessionMessagingKit/Storage.swift b/SessionMessagingKit/Storage.swift index 976b7d606..bd37cdc49 100644 --- a/SessionMessagingKit/Storage.swift +++ b/SessionMessagingKit/Storage.swift @@ -1,5 +1,6 @@ import PromiseKit import Sodium +import YapDatabase public protocol SessionMessagingKitStorageProtocol { @@ -19,6 +20,15 @@ public protocol SessionMessagingKitStorageProtocol { func getUser() -> Contact? func getAllContacts() -> Set func getAllContacts(with transaction: YapDatabaseReadTransaction) -> Set + + // MARK: - Blinded Id cache + + func getBlindedIdMapping(with blindedId: String) -> BlindedIdMapping? + func getBlindedIdMapping(with blindedId: String, using transaction: YapDatabaseReadTransaction) -> BlindedIdMapping? + func cacheBlindedIdMapping(_ mapping: BlindedIdMapping) + func cacheBlindedIdMapping(_ mapping: BlindedIdMapping, using transaction: YapDatabaseReadWriteTransaction) + func enumerateBlindedIdMapping(with block: @escaping (BlindedIdMapping, UnsafeMutablePointer) -> ()) + func enumerateBlindedIdMapping(with block: @escaping (BlindedIdMapping, UnsafeMutablePointer) -> (), transaction: YapDatabaseReadTransaction) // MARK: - Closed Groups @@ -72,6 +82,12 @@ public protocol SessionMessagingKitStorageProtocol { func getOpenGroupInboxLatestMessageId(for server: String) -> Int64? func setOpenGroupInboxLatestMessageId(for server: String, to newValue: Int64, using transaction: Any) func removeOpenGroupInboxLatestMessageId(for server: String, using transaction: Any) + + // MARK: - -- Open Group Outbox Latest Message Id + + func getOpenGroupOutboxLatestMessageId(for server: String) -> Int64? + func setOpenGroupOutboxLatestMessageId(for server: String, to newValue: Int64, using transaction: Any) + func removeOpenGroupOutboxLatestMessageId(for server: String, using transaction: Any) // MARK: - Message Handling diff --git a/SessionMessagingKit/Utilities/ContactUtilities.swift b/SessionMessagingKit/Utilities/ContactUtilities.swift new file mode 100644 index 000000000..704fb3813 --- /dev/null +++ b/SessionMessagingKit/Utilities/ContactUtilities.swift @@ -0,0 +1,99 @@ +import SessionUtilitiesKit + +enum ContactUtilities { + private static func approvedContact(in threadObject: Any, using transaction: Any) -> Contact? { + guard let thread: TSContactThread = threadObject as? TSContactThread else { return nil } + guard thread.shouldBeVisible else { return nil } + guard let contact: Contact = Storage.shared.getContact(with: thread.contactSessionID(), using: transaction) else { + return nil + } + guard contact.didApproveMe else { return nil } + + return contact + } + + static func getAllContacts() -> [String] { + // Collect all contacts + var result: [Contact] = [] + Storage.read { transaction in + TSContactThread.enumerateCollectionObjects(with: transaction) { object, _ in + guard let contact: Contact = approvedContact(in: object, using: transaction) else { return } + + result.append(contact) + } + } + func getDisplayName(for publicKey: String) -> String { + return Storage.shared.getContact(with: publicKey)?.displayName(for: .regular) ?? publicKey + } + + // Remove the current user + if let index = result.firstIndex(where: { $0.sessionID == getUserHexEncodedPublicKey() }) { + result.remove(at: index) + } + + // Sort alphabetically + return result + .sorted(by: { lhs, rhs in + (lhs.displayName(for: .regular) ?? lhs.sessionID) < (rhs.displayName(for: .regular) ?? rhs.sessionID) + }) + .map { $0.sessionID } + } + + static func enumerateApprovedContactThreads(with block: @escaping (TSContactThread, Contact, UnsafeMutablePointer) -> ()) { + Storage.read { transaction in + TSContactThread.enumerateCollectionObjects(with: transaction) { object, stop in + guard let contactThread: TSContactThread = object as? TSContactThread else { return } + guard let contact: Contact = approvedContact(in: object, using: transaction) else { return } + + block(contactThread, contact, stop) + } + } + } + + static func mapping(for blindedId: String, serverPublicKey: String, using dependencies: OpenGroupAPI.Dependencies = OpenGroupAPI.Dependencies()) -> BlindedIdMapping? { + // TODO: Ensure the above case isn't going to be an issue due to legacy messages?. + // Unfortunately the whole point of id-blinding is to make it hard to reverse-engineer a standard + // sessionId, as a result in order to see if there is an unblinded contact for this blindedId we + // can only really generate blinded ids for each contact and check if any match + // + // Due to this we have made a few optimisations to try and early-out as often as possible, first + // we try to retrieve a direct cached mapping + var mappingResult: BlindedIdMapping? = dependencies.storage.getBlindedIdMapping(with: blindedId) + + // No need to continue if we already have a result + if let mapping: BlindedIdMapping = mappingResult { return mapping } + + // Then we try loop through all approved contact threads to see if one of those contacts can be blinded to match + ContactUtilities.enumerateApprovedContactThreads { contactThread, contact, stop in + guard dependencies.sodium.sessionId(contact.sessionID, matchesBlindedId: blindedId, serverPublicKey: serverPublicKey) else { + return + } + + // Cache the mapping + let newMapping: BlindedIdMapping = BlindedIdMapping(blindedId: blindedId, sessionId: contact.sessionID, serverPublicKey: serverPublicKey) + dependencies.storage.cacheBlindedIdMapping(newMapping) + mappingResult = newMapping + stop.pointee = true + } + + // Finish if we have a result + if let mapping: BlindedIdMapping = mappingResult { return mapping } + + // Lastly loop through existing id mappings (in case the user is looking at a different SOGS but once had + // a thread with this contact in a different SOGS and had cached the mapping) + dependencies.storage.enumerateBlindedIdMapping { mapping, stop in + guard mapping.serverPublicKey != serverPublicKey else { return } + guard dependencies.sodium.sessionId(mapping.sessionId, matchesBlindedId: blindedId, serverPublicKey: serverPublicKey) else { + return + } + + // Cache the new mapping + let newMapping: BlindedIdMapping = BlindedIdMapping(blindedId: blindedId, sessionId: mapping.sessionId, serverPublicKey: serverPublicKey) + dependencies.storage.cacheBlindedIdMapping(newMapping) + mappingResult = newMapping + stop.pointee = true + } + + return mappingResult + } +}