From 17a9e510c57f3e141845a3eb7ef2774fd2eea3a3 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Mon, 7 Mar 2022 17:43:30 +1100 Subject: [PATCH] Further work on unit tests (and a couple of bug fixes found when testing) Removed a couple remaining TODOs Added 'standardUserDefaults' to the 'Dependencies' type Tweaked the OpenGroupAPI to only update the 'lastOpen' timestamp if it successfully polls Refactored a couple of methods in the ConversationViewItem into swift so we can clean up the OpenGroupAPI more Updated the OpenGroupAPI so it no longer has static variables for state (shifted to the OpenGroupManager and made them instance variables) Fixed an encoding issue with the Capabilities.Capability --- Session.xcodeproj/project.pbxproj | 25 +- .../ConversationViewItem+Refactor.swift | 126 ++ Session/Conversations/ConversationViewItem.h | 4 - Session/Conversations/ConversationViewItem.m | 103 -- .../Open Groups/OpenGroupSuggestionGrid.swift | 3 +- .../Open Groups/Models/Capabilities.swift | 8 +- .../Open Groups/Models/PinnedMessage.swift | 2 +- .../Open Groups/Models/Room.swift | 2 +- .../Open Groups/OpenGroupAPI+ObjC.swift | 8 - .../Open Groups/OpenGroupAPI.swift | 104 +- .../Open Groups/OpenGroupManager.swift | 80 +- .../Open Groups/Types/Dependencies.swift | 11 + .../Open Groups/Types/SOGSEndpoint.swift | 8 +- .../MessageReceiver+Handling.swift | 9 - .../Pollers/OpenGroupPoller.swift | 17 +- .../Open Groups/OpenGroupAPISpec.swift | 1033 +++++++++++++---- .../_TestUtilities/TestUserDefaults.swift | 27 + SessionSnodeKit/OnionRequestAPI.swift | 3 - .../General/SNUserDefaults.swift | 25 +- 19 files changed, 1118 insertions(+), 480 deletions(-) create mode 100644 Session/Conversations/ConversationViewItem+Refactor.swift delete mode 100644 SessionMessagingKit/Open Groups/OpenGroupAPI+ObjC.swift create mode 100644 SessionMessagingKitTests/_TestUtilities/TestUserDefaults.swift diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index 95257a5c4..266ed7a9f 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -753,7 +753,6 @@ C3DAB3242480CB2B00725F25 /* SRCopyableLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3DAB3232480CB2A00725F25 /* SRCopyableLabel.swift */; }; C3DB66AC260ACA42001EFC55 /* OpenGroupManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3DB66AB260ACA42001EFC55 /* OpenGroupManager.swift */; }; C3DB66C3260ACCE6001EFC55 /* OpenGroupPoller.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3DB66C2260ACCE6001EFC55 /* OpenGroupPoller.swift */; }; - C3DB66CC260AF1F3001EFC55 /* OpenGroupAPI+ObjC.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3DB66CB260AF1F3001EFC55 /* OpenGroupAPI+ObjC.swift */; }; C3DFFAC623E96F0D0058DAF8 /* Sheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3DFFAC523E96F0D0058DAF8 /* Sheet.swift */; }; C3E5C2FA251DBABB0040DFFC /* EditClosedGroupVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3E5C2F9251DBABB0040DFFC /* EditClosedGroupVC.swift */; }; C3ECBF7B257056B700EA7FCE /* Threading.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3ECBF7A257056B700EA7FCE /* Threading.swift */; }; @@ -795,6 +794,8 @@ FD83B9C927D0487A005E1583 /* SendDirectMessageResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD83B9C827D0487A005E1583 /* SendDirectMessageResponse.swift */; }; FD83B9CC27D179BC005E1583 /* FSEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD83B9CB27D179BC005E1583 /* FSEndpoint.swift */; }; FD83B9CE27D17A04005E1583 /* Request.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD83B9CD27D17A04005E1583 /* Request.swift */; }; + FD83B9D227D59495005E1583 /* TestUserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD83B9D127D59495005E1583 /* TestUserDefaults.swift */; }; + FD83B9D427D5A7D5005E1583 /* ConversationViewItem+Refactor.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD83B9D327D5A7D5005E1583 /* ConversationViewItem+Refactor.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 */; }; @@ -990,13 +991,6 @@ remoteGlobalIDString = C3C2A678255388CC00C340D1; remoteInfo = SessionUtilitiesKit; }; - FDC438BA27BB276F00C60D73 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = D221A080169C9E5E00537ABF /* Project object */; - proxyType = 1; - remoteGlobalIDString = D221A088169C9E5E00537ABF; - remoteInfo = Session; - }; /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -1866,7 +1860,6 @@ C3DAB3232480CB2A00725F25 /* SRCopyableLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SRCopyableLabel.swift; sourceTree = ""; }; C3DB66AB260ACA42001EFC55 /* OpenGroupManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenGroupManager.swift; sourceTree = ""; }; C3DB66C2260ACCE6001EFC55 /* OpenGroupPoller.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenGroupPoller.swift; sourceTree = ""; }; - C3DB66CB260AF1F3001EFC55 /* OpenGroupAPI+ObjC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OpenGroupAPI+ObjC.swift"; sourceTree = ""; }; C3DFFAC523E96F0D0058DAF8 /* Sheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Sheet.swift; sourceTree = ""; }; C3E5C2F9251DBABB0040DFFC /* EditClosedGroupVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditClosedGroupVC.swift; sourceTree = ""; }; C3E7134E251C867C009649BB /* Sodium+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Sodium+Utilities.swift"; sourceTree = ""; }; @@ -1933,6 +1926,8 @@ FD83B9C827D0487A005E1583 /* SendDirectMessageResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendDirectMessageResponse.swift; sourceTree = ""; }; FD83B9CB27D179BC005E1583 /* FSEndpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FSEndpoint.swift; sourceTree = ""; }; FD83B9CD27D17A04005E1583 /* Request.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Request.swift; sourceTree = ""; }; + FD83B9D127D59495005E1583 /* TestUserDefaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestUserDefaults.swift; sourceTree = ""; }; + FD83B9D327D5A7D5005E1583 /* ConversationViewItem+Refactor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ConversationViewItem+Refactor.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 = ""; }; @@ -2410,6 +2405,7 @@ B8D84E9325DF72AF005A043E /* ConversationViewAction.h */, 34D1F06F1F8678AA0066283D /* ConversationViewItem.h */, 34D1F0701F8678AA0066283D /* ConversationViewItem.m */, + FD83B9D327D5A7D5005E1583 /* ConversationViewItem+Refactor.swift */, 341341ED2187467900192D59 /* ConversationViewModel.h */, 341341EE2187467900192D59 /* ConversationViewModel.m */, 34ABC0E321DD20C500ED9469 /* ConversationMessageMapping.swift */, @@ -3378,7 +3374,6 @@ FDC4381827B34EAD00C60D73 /* Models */, FDC4380727B31D3A00C60D73 /* Types */, B88FA7B726045D100049422F /* OpenGroupAPI.swift */, - C3DB66CB260AF1F3001EFC55 /* OpenGroupAPI+ObjC.swift */, C3DB66AB260ACA42001EFC55 /* OpenGroupManager.swift */, ); path = "Open Groups"; @@ -3992,6 +3987,7 @@ FD859EF727C2F58900510D0C /* TestAeadXChaCha20Poly1305Ietf.swift */, FD859EF927C2F5C500510D0C /* TestGenericHash.swift */, FD859EFB27C2F60700510D0C /* TestEd25519.swift */, + FD83B9D127D59495005E1583 /* TestUserDefaults.swift */, ); path = _TestUtilities; sourceTree = ""; @@ -4376,7 +4372,6 @@ ); dependencies = ( FDC4389427B9FFC700C60D73 /* PBXTargetDependency */, - FDC438BB27BB276F00C60D73 /* PBXTargetDependency */, ); name = SessionMessagingKitTests; productName = SessionMessagingKitTests; @@ -5327,7 +5322,6 @@ C32C599E256DB02B003C73A2 /* TypingIndicators.swift in Sources */, FDC4380927B31D4E00C60D73 /* SOGSError.swift in Sources */, FDC4382027B36ADC00C60D73 /* SOGSEndpoint.swift in Sources */, - C3DB66CC260AF1F3001EFC55 /* OpenGroupAPI+ObjC.swift in Sources */, C32C5BEF256DC8EE003C73A2 /* OWSDisappearingMessagesJob.m in Sources */, C34A977425A3E34A00852C71 /* ClosedGroupControlMessage.swift in Sources */, FDC4384C27B47F7700C60D73 /* OpenGroup.swift in Sources */, @@ -5547,6 +5541,7 @@ C35E8AAE2485E51D00ACB629 /* IP2Country.swift in Sources */, B835249B25C3AB650089A44F /* VisibleMessageCell.swift in Sources */, 340FC8AE204DAC8D007AEB0F /* OWSSoundSettingsViewController.m in Sources */, + FD83B9D427D5A7D5005E1583 /* ConversationViewItem+Refactor.swift in Sources */, B8D0A25025E3678700C1835E /* LinkDeviceVC.swift in Sources */, 3496957321A301A100DCFE74 /* OWSBackupJob.m in Sources */, B894D0752339EDCF00B4D94D /* NukeDataModal.swift in Sources */, @@ -5594,6 +5589,7 @@ FDC438BD27BB2AB400C60D73 /* Mockable.swift in Sources */, FD859EF627C2F52C00510D0C /* TestSign.swift in Sources */, FDC4389D27BA01F000C60D73 /* TestStorage.swift in Sources */, + FD83B9D227D59495005E1583 /* TestUserDefaults.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -5703,11 +5699,6 @@ target = C3C2A678255388CC00C340D1 /* SessionUtilitiesKit */; targetProxy = FDC438A027BA2B8A00C60D73 /* PBXContainerItemProxy */; }; - FDC438BB27BB276F00C60D73 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = D221A088169C9E5E00537ABF /* Session */; - targetProxy = FDC438BA27BB276F00C60D73 /* PBXContainerItemProxy */; - }; /* End PBXTargetDependency section */ /* Begin PBXVariantGroup section */ diff --git a/Session/Conversations/ConversationViewItem+Refactor.swift b/Session/Conversations/ConversationViewItem+Refactor.swift new file mode 100644 index 000000000..1c47d6cda --- /dev/null +++ b/Session/Conversations/ConversationViewItem+Refactor.swift @@ -0,0 +1,126 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation +import SessionSnodeKit +import SessionMessagingKit + +extension ConversationViewItem { + func deleteLocallyAction() { + guard let message: TSMessage = self.interaction as? TSMessage else { return } + + Storage.write { transaction in + MessageInvalidator.invalidate(message, with: transaction) + message.remove(with: transaction) + + if message.interactionType() == .outgoingMessage { + Storage.shared.cancelPendingMessageSendJobIfNeeded(for: message.timestamp, using: transaction) + } + } + } + + func deleteRemotelyAction() { + guard let message: TSMessage = self.interaction as? TSMessage else { return } + + if isGroupThread { + guard let groupThread: TSGroupThread = message.thread as? TSGroupThread else { return } + + // Only allow deletion on incoming and outgoing messages + guard message.interactionType() == .incomingMessage || message.interactionType() == .outgoingMessage else { + return + } + + if groupThread.isOpenGroup { + // Make sure it's an open group message and get the open group + guard message.isOpenGroupMessage, let uniqueId: String = groupThread.uniqueId, let openGroup: OpenGroup = Storage.shared.getOpenGroup(for: uniqueId) else { + return + } + + // If it's an incoming message the user must have moderator status + if message.interactionType() == .incomingMessage { + guard let userPublicKey: String = Storage.shared.getUserPublicKey() else { return } + + if !OpenGroupManager.isUserModeratorOrAdmin(userPublicKey, for: openGroup.room, on: openGroup.server) { + return + } + } + + // Delete the message + OpenGroupAPI.messageDelete(message.openGroupServerMessageID, in: openGroup.room, on: openGroup.server) + .catch { _ in + // Roll back + message.save() + } + .retainUntilComplete() + } + else { + guard let serverHash: String = message.serverHash else { return } + + let groupPublicKey: String = LKGroupUtilities.getDecodedGroupID(groupThread.groupModel.groupId) + + SnodeAPI.deleteMessage(publicKey: groupPublicKey, serverHashes: [serverHash]) + .catch { _ in + // Roll back + message.save() + } + .retainUntilComplete() + } + } + else { + guard let contactThread: TSContactThread = message.thread as? TSContactThread, let serverHash: String = message.serverHash else { + return + } + + SnodeAPI.deleteMessage(publicKey: contactThread.contactSessionID(), serverHashes: [serverHash]) + .catch { _ in + // Roll back + message.save() + } + .retainUntilComplete() + } + } + + // Remove this after the unsend request is enabled + func deleteAction() { + Storage.write { transaction in + self.interaction.remove(with: transaction) + + if self.interaction.interactionType() == .outgoingMessage { + Storage.shared.cancelPendingMessageSendJobIfNeeded(for: self.interaction.timestamp, using: transaction) + } + } + + + if self.isGroupThread { + guard let message: TSMessage = self.interaction as? TSMessage, let groupThread: TSGroupThread = message.thread as? TSGroupThread else { + return + } + + // Only allow deletion on incoming and outgoing messages + guard message.interactionType() == .incomingMessage || message.interactionType() == .outgoingMessage else { + return + } + + // Make sure it's an open group message and get the open group + guard message.isOpenGroupMessage, let uniqueId: String = groupThread.uniqueId, let openGroup: OpenGroup = Storage.shared.getOpenGroup(for: uniqueId) else { + return + } + + // If it's an incoming message the user must have moderator status + if message.interactionType() == .incomingMessage { + guard let userPublicKey: String = Storage.shared.getUserPublicKey() else { return } + + if !OpenGroupManager.isUserModeratorOrAdmin(userPublicKey, for: openGroup.room, on: openGroup.server) { + return + } + } + + // Delete the message + OpenGroupAPI.messageDelete(message.openGroupServerMessageID, in: openGroup.room, on: openGroup.server) + .catch { _ in + // Roll back + message.save() + } + .retainUntilComplete() + } + } +} diff --git a/Session/Conversations/ConversationViewItem.h b/Session/Conversations/ConversationViewItem.h index 8aeaccd01..069dcf99f 100644 --- a/Session/Conversations/ConversationViewItem.h +++ b/Session/Conversations/ConversationViewItem.h @@ -133,10 +133,6 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType); - (void)copyTextAction; - (void)shareMediaAction; - (void)saveMediaAction; -- (void)deleteLocallyAction; -- (void)deleteRemotelyAction; - -- (void)deleteAction; // Remove this after the unsend request is enabled - (BOOL)canCopyMedia; - (BOOL)canSaveMedia; diff --git a/Session/Conversations/ConversationViewItem.m b/Session/Conversations/ConversationViewItem.m index 9fe60412b..43a68403c 100644 --- a/Session/Conversations/ConversationViewItem.m +++ b/Session/Conversations/ConversationViewItem.m @@ -972,109 +972,6 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType) return [self saveMediaAlbumItems:mediaAlbumItems]; } -- (void)deleteLocallyAction -{ - TSMessage *message = (TSMessage *)self.interaction; - [LKStorage writeWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { - [MessageInvalidator invalidate:message with:transaction]; - [self.interaction removeWithTransaction:transaction]; - if (self.interaction.interactionType == OWSInteractionType_OutgoingMessage) { - [LKStorage.shared cancelPendingMessageSendJobIfNeededForMessage:self.interaction.timestamp using:transaction]; - } - }]; -} - -- (void)deleteRemotelyAction -{ - TSMessage *message = (TSMessage *)self.interaction; - - if (self.isGroupThread) { - TSGroupThread *groupThread = (TSGroupThread *)self.interaction.thread; - - // Only allow deletion on incoming and outgoing messages - OWSInteractionType interationType = self.interaction.interactionType; - if (interationType != OWSInteractionType_IncomingMessage && interationType != OWSInteractionType_OutgoingMessage) return; - - if (groupThread.isOpenGroup) { - // Make sure it's an open group message - if (!message.isOpenGroupMessage) return; - - // Get the open group - SNOpenGroupV2 *openGroup = [LKStorage.shared getOpenGroupForThreadID:groupThread.uniqueId]; - if (openGroup == nil) return; - - // If it's an incoming message the user must have moderator status - if (self.interaction.interactionType == OWSInteractionType_IncomingMessage) { - NSString *userPublicKey = [LKStorage.shared getUserPublicKey]; - if (![SNOpenGroupManager isUserModeratorOrAdmin:userPublicKey forRoom:openGroup.room onServer:openGroup.server]) { return; } - } - - // Delete the message - [[SNOpenGroupAPI deleteMessageWithServerID:message.openGroupServerMessageID fromRoom:openGroup.room onServer:openGroup.server].catch(^(NSError *error) { - // Roll back - [self.interaction save]; - }) retainUntilComplete]; - } else { - NSString *groupPublicKey = [LKGroupUtilities getDecodedGroupID:groupThread.groupModel.groupId]; - [[SNSnodeAPI deleteMessageForPublickKey:groupPublicKey serverHashes:@[message.serverHash]].catch(^(NSError *error) { - // Roll back - [self.interaction save]; - }) retainUntilComplete]; - } - } else { - TSContactThread *contactThread = (TSContactThread *)self.interaction.thread; - [[SNSnodeAPI deleteMessageForPublickKey:contactThread.contactSessionID serverHashes:@[message.serverHash]].catch(^(NSError *error) { - // Roll back - [self.interaction save]; - }) retainUntilComplete]; - } - -} - -// Remove this after the unsend request is enabled -- (void)deleteAction -{ - [LKStorage writeWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { - [self.interaction removeWithTransaction:transaction]; - if (self.interaction.interactionType == OWSInteractionType_OutgoingMessage) { - [LKStorage.shared cancelPendingMessageSendJobIfNeededForMessage:self.interaction.timestamp using:transaction]; - } - }]; - - if (self.isGroupThread) { - TSGroupThread *groupThread = (TSGroupThread *)self.interaction.thread; - - // Only allow deletion on incoming and outgoing messages - OWSInteractionType interationType = self.interaction.interactionType; - if (interationType != OWSInteractionType_IncomingMessage && interationType != OWSInteractionType_OutgoingMessage) return; - - // Make sure it's an open group message - TSMessage *message = (TSMessage *)self.interaction; - if (!message.isOpenGroupMessage) return; - - // Get the open group - SNOpenGroupV2 *openGroup = [LKStorage.shared getOpenGroupForThreadID:groupThread.uniqueId]; - if (openGroup == nil && openGroup == nil) return; - - // If it's an incoming message the user must have moderator status - if (self.interaction.interactionType == OWSInteractionType_IncomingMessage) { - NSString *userPublicKey = [LKStorage.shared getUserPublicKey]; - if (openGroup != nil) { - if (![SNOpenGroupManager isUserModeratorOrAdmin:userPublicKey forRoom:openGroup.room onServer:openGroup.server]) { return; } - } - } - - // Delete the message - BOOL wasSentByUser = (interationType == OWSInteractionType_OutgoingMessage); - if (openGroup != nil) { - [[SNOpenGroupAPI deleteMessageWithServerID:message.openGroupServerMessageID fromRoom:openGroup.room onServer:openGroup.server].catch(^(NSError *error) { - // Roll back - [self.interaction save]; - }) retainUntilComplete]; - } - } -} - - (BOOL)hasBodyTextActionContent { return self.hasBodyText && self.displayableBodyText.fullText.length > 0; diff --git a/Session/Open Groups/OpenGroupSuggestionGrid.swift b/Session/Open Groups/OpenGroupSuggestionGrid.swift index f0e4fd077..90c807ba6 100644 --- a/Session/Open Groups/OpenGroupSuggestionGrid.swift +++ b/Session/Open Groups/OpenGroupSuggestionGrid.swift @@ -1,5 +1,6 @@ import PromiseKit import NVActivityIndicatorView +import SessionMessagingKit final class OpenGroupSuggestionGrid : UIView, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout { private let maxWidth: CGFloat @@ -64,7 +65,7 @@ final class OpenGroupSuggestionGrid : UIView, UICollectionViewDataSource, UIColl widthAnchor.constraint(greaterThanOrEqualToConstant: OpenGroupSuggestionGrid.cellHeight).isActive = true OpenGroupManager.getDefaultRoomsIfNeeded() - _ = OpenGroupManager.defaultRoomsPromise?.done { [weak self] rooms in + _ = OpenGroupManager.shared.cache.defaultRoomsPromise?.done { [weak self] rooms in self?.rooms = rooms } } diff --git a/SessionMessagingKit/Open Groups/Models/Capabilities.swift b/SessionMessagingKit/Open Groups/Models/Capabilities.swift index 6cf98b51d..30da0117b 100644 --- a/SessionMessagingKit/Open Groups/Models/Capabilities.swift +++ b/SessionMessagingKit/Open Groups/Models/Capabilities.swift @@ -3,7 +3,7 @@ import Foundation extension OpenGroupAPI { - public struct Capabilities: Codable { + public struct Capabilities: Codable, Equatable { public enum Capability: Equatable, CaseIterable, Codable { public static var allCases: [Capability] { [.sogs, .blind] @@ -54,4 +54,10 @@ extension OpenGroupAPI.Capabilities.Capability { self = OpenGroupAPI.Capabilities.Capability(from: valueString) } + + public func encode(to encoder: Encoder) throws { + var container: SingleValueEncodingContainer = encoder.singleValueContainer() + + try container.encode(rawValue) + } } diff --git a/SessionMessagingKit/Open Groups/Models/PinnedMessage.swift b/SessionMessagingKit/Open Groups/Models/PinnedMessage.swift index 8ccdec795..e8f1f7a8e 100644 --- a/SessionMessagingKit/Open Groups/Models/PinnedMessage.swift +++ b/SessionMessagingKit/Open Groups/Models/PinnedMessage.swift @@ -3,7 +3,7 @@ import Foundation extension OpenGroupAPI { - public struct PinnedMessage: Codable { + public struct PinnedMessage: Codable, Equatable { enum CodingKeys: String, CodingKey { case id case pinnedAt = "pinned_at" diff --git a/SessionMessagingKit/Open Groups/Models/Room.swift b/SessionMessagingKit/Open Groups/Models/Room.swift index 06417848b..535c76902 100644 --- a/SessionMessagingKit/Open Groups/Models/Room.swift +++ b/SessionMessagingKit/Open Groups/Models/Room.swift @@ -3,7 +3,7 @@ import Foundation extension OpenGroupAPI { - public struct Room: Codable { + public struct Room: Codable, Equatable { enum CodingKeys: String, CodingKey { case token case name diff --git a/SessionMessagingKit/Open Groups/OpenGroupAPI+ObjC.swift b/SessionMessagingKit/Open Groups/OpenGroupAPI+ObjC.swift deleted file mode 100644 index 43b0b9ca5..000000000 --- a/SessionMessagingKit/Open Groups/OpenGroupAPI+ObjC.swift +++ /dev/null @@ -1,8 +0,0 @@ -import PromiseKit - -extension OpenGroupAPI { - @objc(deleteMessageWithServerID:fromRoom:onServer:) - public static func objc_deleteMessage(with serverID: Int64, from room: String, on server: String) -> AnyPromise { - return AnyPromise.from(messageDelete(serverID, in: room, on: server)) - } -} diff --git a/SessionMessagingKit/Open Groups/OpenGroupAPI.swift b/SessionMessagingKit/Open Groups/OpenGroupAPI.swift index 7ac3f7e94..4962e1f47 100644 --- a/SessionMessagingKit/Open Groups/OpenGroupAPI.swift +++ b/SessionMessagingKit/Open Groups/OpenGroupAPI.swift @@ -3,34 +3,14 @@ import SessionSnodeKit import Sodium import Curve25519Kit -@objc(SNOpenGroupAPI) -public final class OpenGroupAPI: NSObject { - +public enum OpenGroupAPI { // MARK: - Settings public static let defaultServer = "http://116.203.70.33" public static let defaultServerPublicKey = "a03c383cf63c3c4efe67acc52112a6dd734b3a946b9545f488aaa93da7991238" public static let workQueue = DispatchQueue(label: "OpenGroupAPI.workQueue", qos: .userInitiated) // It's important that this is a serial queue - - // MARK: - Polling State - - private static var hasPerformedInitialPoll: Atomic<[String: Bool]> = Atomic([:]) - private static var timeSinceLastPoll: Atomic<[String: TimeInterval]> = Atomic([:]) - private static var lastPollTime: Atomic = Atomic(.greatestFiniteMagnitude) - private static let timeSinceLastOpen: Atomic = { - guard let lastOpen = UserDefaults.standard[.lastOpen] else { return Atomic(.greatestFiniteMagnitude) } - - return Atomic(Date().timeIntervalSince(lastOpen)) - }() - - - // TODO: Remove these - private static var legacyAuthTokenPromises: Atomic<[String: Promise]> = Atomic([:]) - private static var legacyHasUpdatedLastOpenDate = false - private static var legacyGroupImagePromises: [String: Promise] = [:] - // MARK: - Batching & Polling @@ -42,20 +22,17 @@ public final class OpenGroupAPI: NSObject { /// - 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)) + public static func poll( + _ server: String, + hasPerformedInitialPoll: Bool, + timeSinceLastPoll: TimeInterval, + using dependencies: Dependencies = Dependencies() + ) -> Promise<[Endpoint: (OnionRequestResponseInfoType, Codable?)]> { 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 } - lastPollTime.mutate { $0 = min($0, timeSinceLastOpen.wrappedValue)} - UserDefaults.standard[.lastOpen] = Date() - // Generate the requests let requestResponseType: [BatchRequestInfoType] = [ BatchRequestInfo( @@ -78,8 +55,8 @@ public final class OpenGroupAPI: NSObject { // If it's the first poll for this launch and it's been longer than // 'maxInactivityPeriod' then just retrieve recent messages instead // of trying to get all messages since the last one retrieved - !hadPerformedInitialPoll && - originalTimeSinceLastPoll > OpenGroupAPI.Poller.maxInactivityPeriod + !hasPerformedInitialPoll && + timeSinceLastPoll > OpenGroupAPI.Poller.maxInactivityPeriod ) ) @@ -180,7 +157,6 @@ public final class OpenGroupAPI: NSObject { body: requestBody ) - // TODO: Handle a `412` response (ie. a required capability isn't supported) return send(request, using: dependencies) .decoded(as: responseTypes, on: OpenGroupAPI.workQueue, using: dependencies) .map { result in @@ -203,11 +179,9 @@ public final class OpenGroupAPI: NSObject { public static func capabilities(on server: String, using dependencies: Dependencies = Dependencies()) -> Promise<(OnionRequestResponseInfoType, Capabilities)> { let request: Request = Request( server: server, - endpoint: .capabilities, - queryParameters: [:] // TODO: Add any requirements '.required'. + endpoint: .capabilities ) - // TODO: Handle a `412` response (ie. a required capability isn't supported) return send(request, using: dependencies) .decoded(as: Capabilities.self, on: OpenGroupAPI.workQueue, using: dependencies) } @@ -290,31 +264,21 @@ public final class OpenGroupAPI: NSObject { ] return sequence(server, requests: requestResponseType, using: dependencies) - .map { response -> (capabilities: (OnionRequestResponseInfoType, Capabilities?), room: (OnionRequestResponseInfoType, Room?)) in - var capabilities: (OnionRequestResponseInfoType, Capabilities?)? = nil - var room: (OnionRequestResponseInfoType, Room?)? = nil + .map { (response: [Endpoint: (OnionRequestResponseInfoType, Codable?)]) -> (capabilities: (OnionRequestResponseInfoType, Capabilities?), room: (OnionRequestResponseInfoType, Room?)) in + let maybeCapabilities: (OnionRequestResponseInfoType, Capabilities?)? = response[.capabilities] + .map { info, data in (info, (data as? BatchSubResponse)?.body) } + let maybeRoomResponse: (OnionRequestResponseInfoType, Codable?)? = response + .first(where: { key, _ in + switch key { + case .room: return true + default: return false + } + }) + .map { _, value in value } + let maybeRoom: (OnionRequestResponseInfoType, Room?)? = maybeRoomResponse + .map { info, data in (info, (data as? BatchSubResponse)?.body) } - try response.forEach { (endpoint: Endpoint, endpointResponse: (info: OnionRequestResponseInfoType, data: Codable?)) in - switch endpoint { - case .capabilities: - guard let responseData: BatchSubResponse = endpointResponse.data as? BatchSubResponse, let responseBody: Capabilities = responseData.body else { - throw HTTP.Error.parsingFailed - } - - capabilities = (endpointResponse.info, responseBody) - - case .room: - guard let responseData: OpenGroupAPI.BatchSubResponse = endpointResponse.data as? OpenGroupAPI.BatchSubResponse, let responseBody: OpenGroupAPI.Room = responseData.body else { - throw HTTP.Error.parsingFailed - } - - room = (endpointResponse.info, responseBody) - - default: break // No custom handling needed - } - } - - guard let capabilities: (OnionRequestResponseInfoType, Capabilities?) = capabilities, let room: (OnionRequestResponseInfoType, Room?) = room else { + guard let capabilities: (OnionRequestResponseInfoType, Capabilities?) = maybeCapabilities, let room: (OnionRequestResponseInfoType, Room?) = maybeRoom else { throw HTTP.Error.parsingFailed } @@ -367,7 +331,7 @@ public final class OpenGroupAPI: NSObject { } /// Returns a single message by ID - public static func message(_ id: Int64, in roomToken: String, on server: String, using dependencies: Dependencies = Dependencies()) -> Promise<(OnionRequestResponseInfoType, Message)> { + public static func message(_ id: UInt64, in roomToken: String, on server: String, using dependencies: Dependencies = Dependencies()) -> Promise<(OnionRequestResponseInfoType, Message)> { let request: Request = Request( server: server, endpoint: .roomMessageIndividual(roomToken, id: id) @@ -381,7 +345,7 @@ public final class OpenGroupAPI: NSObject { /// /// **Note:** This edit may only be initiated by the creator of the post, and the poster must currently have write permissions in the room public static func messageUpdate( - _ id: Int64, + _ id: UInt64, plaintext: Data, fileIds: [Int64]?, in roomToken: String, @@ -405,12 +369,11 @@ public final class OpenGroupAPI: NSObject { body: requestBody ) - // TODO: Handle custom response info? return send(request, using: dependencies) } public static func messageDelete( - _ id: Int64, + _ id: UInt64, in roomToken: String, on server: String, using dependencies: Dependencies = Dependencies() @@ -432,8 +395,6 @@ public final class OpenGroupAPI: NSObject { let request: Request = Request( server: server, endpoint: .roomMessagesRecent(roomToken) - // TODO: Limit?. -// queryParameters: [ .limit: 50 ] ) return send(request, using: dependencies) @@ -444,13 +405,10 @@ public final class OpenGroupAPI: NSObject { /// remove the `@available` line and make sure to route the response of this method to the `OpenGroupManager.handleMessages` /// method to ensure things are processed correctly @available(*, unavailable, message: "Avoid using this directly, use the pre-built `poll()` method instead") - public static func messagesBefore(messageId: Int64, in roomToken: String, on server: String, using dependencies: Dependencies = Dependencies()) -> Promise<(OnionRequestResponseInfoType, [Message])> { - // TODO: Do we need to be able to load old messages? + public static func messagesBefore(messageId: UInt64, in roomToken: String, on server: String, using dependencies: Dependencies = Dependencies()) -> Promise<(OnionRequestResponseInfoType, [Message])> { let request: Request = Request( server: server, endpoint: .roomMessagesBefore(roomToken, id: messageId) - // TODO: Limit?. -// queryParameters: [ .limit: 50 ] ) return send(request, using: dependencies) @@ -465,8 +423,6 @@ public final class OpenGroupAPI: NSObject { let request: Request = Request( server: server, endpoint: .roomMessagesSince(roomToken, seqNo: seqNo) - // TODO: Limit?. -// queryParameters: [ .limit: 50 ] ) return send(request, using: dependencies) @@ -485,7 +441,7 @@ public final class OpenGroupAPI: NSObject { /// Pinned messages that are already pinned will be re-pinned (that is, their pin timestamp and pinning admin user will be updated) - because pinned /// messages are returned in pinning-order this allows admins to order multiple pinned messages in a room by re-pinning (via this endpoint) in the /// order in which pinned messages should be displayed - public static func pinMessage(id: Int64, in roomToken: String, on server: String, using dependencies: Dependencies = Dependencies()) -> Promise { + public static func pinMessage(id: UInt64, in roomToken: String, on server: String, using dependencies: Dependencies = Dependencies()) -> Promise { let request: Request = Request( method: .post, server: server, @@ -499,7 +455,7 @@ public final class OpenGroupAPI: NSObject { /// Remove a message from this room's pinned message list /// /// The user must have `admin` (not just `moderator`) permissions in the room - public static func unpinMessage(id: Int64, in roomToken: String, on server: String, using dependencies: Dependencies = Dependencies()) -> Promise { + public static func unpinMessage(id: UInt64, in roomToken: String, on server: String, using dependencies: Dependencies = Dependencies()) -> Promise { let request: Request = Request( method: .post, server: server, diff --git a/SessionMessagingKit/Open Groups/OpenGroupManager.swift b/SessionMessagingKit/Open Groups/OpenGroupManager.swift index c128a6bc9..f968234ac 100644 --- a/SessionMessagingKit/Open Groups/OpenGroupManager.swift +++ b/SessionMessagingKit/Open Groups/OpenGroupManager.swift @@ -5,22 +5,44 @@ import SessionSnodeKit @objc(SNOpenGroupManager) public final class OpenGroupManager: NSObject { - @objc public static let shared = OpenGroupManager() + public class Cache { + public var defaultRoomsPromise: Promise<[OpenGroupAPI.Room]>? + fileprivate var groupImagePromises: [String: Promise] = [:] + + /// Server URL to room ID to set of user IDs + fileprivate var moderators: [String: [String: Set]] = [:] + fileprivate var admins: [String: [String: Set]] = [:] + + /// Server URL to value + public var hasPerformedInitialPoll: [String: Bool] = [:] + public var timeSinceLastPoll: [String: TimeInterval] = [:] + + fileprivate var _timeSinceLastOpen: TimeInterval? + public func getTimeSinceLastOpen(using dependencies: OpenGroupAPI.Dependencies = OpenGroupAPI.Dependencies()) -> TimeInterval { + if let storedTimeSinceLastOpen: TimeInterval = _timeSinceLastOpen { + return storedTimeSinceLastOpen + } + + guard let lastOpen: Date = dependencies.standardUserDefaults[.lastOpen] else { + _timeSinceLastOpen = .greatestFiniteMagnitude + return .greatestFiniteMagnitude + } + + _timeSinceLastOpen = dependencies.date.timeIntervalSince(lastOpen) + return dependencies.date.timeIntervalSince(lastOpen) + } + } + + // MARK: - Variables + + @objc public static let shared: OpenGroupManager = OpenGroupManager() + + public let mutableCache: Atomic = Atomic(Cache()) + public var cache: Cache { return mutableCache.wrappedValue } private var pollers: [String: OpenGroupAPI.Poller] = [:] // One for each server private var isPolling = false - // MARK: - Cache - - public static var defaultRoomsPromise: Promise<[OpenGroupAPI.Room]>? - private static var groupImagePromises: [String: Promise] = [:] - - /// Server URL to room ID to set of moderator IDs - private static var moderators: Atomic<[String: [String: Set]]> = Atomic([:]) - - /// Server URL to room ID to set of admin IDs - private static var admins: Atomic<[String: [String: Set]]> = Atomic([:]) - // MARK: - Polling @objc public func startPolling() { @@ -247,15 +269,15 @@ public final class OpenGroupManager: NSObject { // - Moderators if let moderators: [String] = (pollInfo.details?.moderators ?? maybeUpdatedModel?.groupModeratorIds) { - OpenGroupManager.moderators.mutate { - $0[server] = ($0[server] ?? [:]).setting(roomToken, Set(moderators)) + OpenGroupManager.shared.mutableCache.mutate { cache in + cache.moderators[server] = (cache.moderators[server] ?? [:]).setting(roomToken, Set(moderators)) } } // - Admins if let admins: [String] = (pollInfo.details?.admins ?? maybeUpdatedModel?.groupAdminIds) { - OpenGroupManager.admins.mutate { - $0[server] = ($0[server] ?? [:]).setting(roomToken, Set(admins)) + OpenGroupManager.shared.mutableCache.mutate { cache in + cache.admins[server] = (cache.admins[server] ?? [:]).setting(roomToken, Set(admins)) } } @@ -458,8 +480,8 @@ public final class OpenGroupManager: NSObject { } public static func isUserModeratorOrAdmin(_ publicKey: String, for room: String, on server: String, using dependencies: OpenGroupAPI.Dependencies = OpenGroupAPI.Dependencies()) -> Bool { - let modAndAdminKeys: Set = (OpenGroupManager.moderators.wrappedValue[server]?[room] ?? Set()) - .union(OpenGroupManager.admins.wrappedValue[server]?[room] ?? Set()) + let modAndAdminKeys: Set = (OpenGroupManager.shared.cache.moderators[server]?[room] ?? Set()) + .union(OpenGroupManager.shared.cache.admins[server]?[room] ?? Set()) // If the publicKey is in the set then return immediately, otherwise only continue if it's the // current user @@ -507,7 +529,7 @@ public final class OpenGroupManager: NSObject { public static func getDefaultRoomsIfNeeded(using dependencies: OpenGroupAPI.Dependencies = OpenGroupAPI.Dependencies()) { // Note: If we already have a 'defaultRoomsPromise' then there is no need to get it again - guard OpenGroupManager.defaultRoomsPromise == nil else { return } + guard OpenGroupManager.shared.cache.defaultRoomsPromise == nil else { return } dependencies.storage.write( with: { transaction in @@ -518,11 +540,13 @@ public final class OpenGroupManager: NSObject { ) }, completion: { - OpenGroupManager.defaultRoomsPromise = attempt(maxRetryCount: 8, recoveringOn: DispatchQueue.main) { - OpenGroupAPI.rooms(for: OpenGroupAPI.defaultServer, using: dependencies) - .map { _, data in data } + OpenGroupManager.shared.mutableCache.mutate { cache in + cache.defaultRoomsPromise = attempt(maxRetryCount: 8, recoveringOn: DispatchQueue.main) { + OpenGroupAPI.rooms(for: OpenGroupAPI.defaultServer, using: dependencies) + .map { _, data in data } + } } - OpenGroupManager.defaultRoomsPromise? + OpenGroupManager.shared.cache.defaultRoomsPromise? .done(on: OpenGroupAPI.workQueue) { items in items .compactMap { room -> (UInt64, String)? in @@ -536,7 +560,9 @@ public final class OpenGroupManager: NSObject { } } .catch(on: OpenGroupAPI.workQueue) { _ in - OpenGroupManager.defaultRoomsPromise = nil + OpenGroupManager.shared.mutableCache.mutate { cache in + cache.defaultRoomsPromise = nil + } } } ) @@ -566,7 +592,7 @@ public final class OpenGroupManager: NSObject { return Promise.value(data) } - if let promise = OpenGroupManager.groupImagePromises["\(server).\(roomToken)"] { + if let promise = OpenGroupManager.shared.cache.groupImagePromises["\(server).\(roomToken)"] { return promise } @@ -581,7 +607,9 @@ public final class OpenGroupManager: NSObject { UserDefaults.standard[.lastOpenGroupImageUpdate] = now } } - OpenGroupManager.groupImagePromises["\(server).\(roomToken)"] = promise + OpenGroupManager.shared.mutableCache.mutate { cache in + cache.groupImagePromises["\(server).\(roomToken)"] = promise + } return promise } diff --git a/SessionMessagingKit/Open Groups/Types/Dependencies.swift b/SessionMessagingKit/Open Groups/Types/Dependencies.swift index 86479e06f..9ba55334c 100644 --- a/SessionMessagingKit/Open Groups/Types/Dependencies.swift +++ b/SessionMessagingKit/Open Groups/Types/Dependencies.swift @@ -3,6 +3,7 @@ import Foundation import Sodium import SessionSnodeKit +import SessionUtilitiesKit // MARK: - Dependencies @@ -62,6 +63,12 @@ extension OpenGroupAPI { set { _nonceGenerator24 = newValue } } + private var _standardUserDefaults: UserDefaultsType? + public var standardUserDefaults: UserDefaultsType { + get { getValueSettingIfNull(&_standardUserDefaults) { UserDefaults.standard } } + set { _standardUserDefaults = newValue } + } + private var _date: Date? public var date: Date { get { getValueSettingIfNull(&_date) { Date() } } @@ -80,6 +87,7 @@ extension OpenGroupAPI { ed25519: Ed25519Type.Type? = nil, nonceGenerator16: NonceGenerator16ByteType? = nil, nonceGenerator24: NonceGenerator24ByteType? = nil, + standardUserDefaults: UserDefaultsType? = nil, date: Date? = nil ) { _api = api @@ -91,6 +99,7 @@ extension OpenGroupAPI { _ed25519 = ed25519 _nonceGenerator16 = nonceGenerator16 _nonceGenerator24 = nonceGenerator24 + _standardUserDefaults = standardUserDefaults _date = date } @@ -106,6 +115,7 @@ extension OpenGroupAPI { ed25519: Ed25519Type.Type? = nil, nonceGenerator16: NonceGenerator16ByteType? = nil, nonceGenerator24: NonceGenerator24ByteType? = nil, + standardUserDefaults: UserDefaultsType? = nil, date: Date? = nil ) -> Dependencies { return Dependencies( @@ -118,6 +128,7 @@ extension OpenGroupAPI { ed25519: (ed25519 ?? self._ed25519), nonceGenerator16: (nonceGenerator16 ?? self._nonceGenerator16), nonceGenerator24: (nonceGenerator24 ?? self._nonceGenerator24), + standardUserDefaults: (standardUserDefaults ?? self._standardUserDefaults), date: (date ?? self._date) ) } diff --git a/SessionMessagingKit/Open Groups/Types/SOGSEndpoint.swift b/SessionMessagingKit/Open Groups/Types/SOGSEndpoint.swift index 10c5088d3..a1b8c9e94 100644 --- a/SessionMessagingKit/Open Groups/Types/SOGSEndpoint.swift +++ b/SessionMessagingKit/Open Groups/Types/SOGSEndpoint.swift @@ -20,15 +20,15 @@ extension OpenGroupAPI { // Messages case roomMessage(String) - case roomMessageIndividual(String, id: Int64) + case roomMessageIndividual(String, id: UInt64) case roomMessagesRecent(String) - case roomMessagesBefore(String, id: Int64) + case roomMessagesBefore(String, id: UInt64) case roomMessagesSince(String, seqNo: Int64) // Pinning - case roomPinMessage(String, id: Int64) - case roomUnpinMessage(String, id: Int64) + case roomPinMessage(String, id: UInt64) + case roomUnpinMessage(String, id: UInt64) case roomUnpinAll(String) // Files diff --git a/SessionMessagingKit/Sending & Receiving/MessageReceiver+Handling.swift b/SessionMessagingKit/Sending & Receiving/MessageReceiver+Handling.swift index 3e1c1b283..4d9b631f5 100644 --- a/SessionMessagingKit/Sending & Receiving/MessageReceiver+Handling.swift +++ b/SessionMessagingKit/Sending & Receiving/MessageReceiver+Handling.swift @@ -240,14 +240,6 @@ extension MessageReceiver { thread.remove(with: transaction) } } - else if SessionId.Prefix(from: sessionID) != .blinded { - // Otherwise create and save the thread (if the contact isn't a blinded contact - we don't want to - // auto-create threads for blinded contacts if they have no messages) - // TODO: See what this will do with blinded->unblinded conversations? - let thread = TSContactThread.getOrCreateThread(withContactSessionID: sessionID, transaction: transaction) - thread.shouldBeVisible = true - thread.save(with: transaction) - } } // FIXME: 'OWSBlockingManager' manages it's own dbConnection and transactions so we have to dispatch this to prevent deadlocks @@ -891,7 +883,6 @@ extension MessageReceiver { // Note: Pending `MessageSendJobs` _shouldn't_ be an issue as even if they are sent after the // un-blinding of a thread, the logic when handling the sent messages should automatically // assign them to the correct thread - // TODO: Validate the above note once `/outbox` has been implemented view.enumerateRows(inGroup: blindedThreadId) { _, _, object, _, _, _ in guard let interaction: TSInteraction = object as? TSInteraction else { return diff --git a/SessionMessagingKit/Sending & Receiving/Pollers/OpenGroupPoller.swift b/SessionMessagingKit/Sending & Receiving/Pollers/OpenGroupPoller.swift index a742a9639..09be07beb 100644 --- a/SessionMessagingKit/Sending & Receiving/Pollers/OpenGroupPoller.swift +++ b/SessionMessagingKit/Sending & Receiving/Pollers/OpenGroupPoller.swift @@ -52,13 +52,28 @@ extension OpenGroupAPI { guard !self.isPolling else { return Promise.value(()) } self.isPolling = true + let server: String = self.server let (promise, seal) = Promise.pending() promise.retainUntilComplete() - OpenGroupAPI.poll(server) + OpenGroupAPI + .poll( + server, + hasPerformedInitialPoll: OpenGroupManager.shared.cache.hasPerformedInitialPoll[server] == true, + timeSinceLastPoll: ( + OpenGroupManager.shared.cache.timeSinceLastPoll[server] ?? + OpenGroupManager.shared.cache.getTimeSinceLastOpen() + ) + ) .done(on: OpenGroupAPI.workQueue) { [weak self] response in self?.isPolling = false self?.handlePollResponse(response, isBackgroundPoll: isBackgroundPoll) + + OpenGroupManager.shared.mutableCache.mutate { cache in + cache.hasPerformedInitialPoll[server] = true + cache.timeSinceLastPoll[server] = Date().timeIntervalSince1970 + UserDefaults.standard[.lastOpen] = Date() + } seal.fulfill(()) } .catch(on: OpenGroupAPI.workQueue) { [weak self] error in diff --git a/SessionMessagingKitTests/Open Groups/OpenGroupAPISpec.swift b/SessionMessagingKitTests/Open Groups/OpenGroupAPISpec.swift index d70018b13..661c0a154 100644 --- a/SessionMessagingKitTests/Open Groups/OpenGroupAPISpec.swift +++ b/SessionMessagingKitTests/Open Groups/OpenGroupAPISpec.swift @@ -83,6 +83,7 @@ class OpenGroupAPISpec: QuickSpec { var testAeadXChaCha20Poly1305Ietf: TestAeadXChaCha20Poly1305Ietf! var testGenericHash: TestGenericHash! var testSign: TestSign! + var testUserDefaults: TestUserDefaults! var dependencies: OpenGroupAPI.Dependencies! var response: (OnionRequestResponseInfoType, Codable)? = nil @@ -98,6 +99,7 @@ class OpenGroupAPISpec: QuickSpec { testAeadXChaCha20Poly1305Ietf = TestAeadXChaCha20Poly1305Ietf() testGenericHash = TestGenericHash() testSign = TestSign() + testUserDefaults = TestUserDefaults() dependencies = OpenGroupAPI.Dependencies( api: TestApi.self, storage: testStorage, @@ -108,6 +110,7 @@ class OpenGroupAPISpec: QuickSpec { ed25519: TestEd25519.self, nonceGenerator16: TestNonce16Generator(), nonceGenerator24: TestNonce24Generator(), + standardUserDefaults: testUserDefaults, date: Date(timeIntervalSince1970: 1234567890) ) @@ -144,8 +147,14 @@ class OpenGroupAPISpec: QuickSpec { } afterEach { - dependencies = nil testStorage = nil + testSodium = nil + testAeadXChaCha20Poly1305Ietf = nil + testGenericHash = nil + testSign = nil + testUserDefaults = nil + dependencies = nil + response = nil pollResponse = nil error = nil @@ -154,74 +163,496 @@ class OpenGroupAPISpec: QuickSpec { // MARK: - Batching & Polling context("when polling") { - it("generates the correct request") { - class LocalTestApi: TestApi { - override class var mockResponse: Data? { - let responses: [Data] = [ - try! JSONEncoder().encode( - OpenGroupAPI.BatchSubResponse( - code: 200, - headers: [:], - body: OpenGroupAPI.Capabilities(capabilities: [], missing: nil), - failedToParseBody: false + context("and given a correct response") { + beforeEach { + class LocalTestApi: TestApi { + override class var mockResponse: Data? { + let responses: [Data] = [ + try! JSONEncoder().encode( + OpenGroupAPI.BatchSubResponse( + code: 200, + headers: [:], + body: OpenGroupAPI.Capabilities(capabilities: [], missing: nil), + failedToParseBody: false + ) + ), + try! JSONEncoder().encode( + OpenGroupAPI.BatchSubResponse( + code: 200, + headers: [:], + body: try! JSONDecoder().decode( + OpenGroupAPI.RoomPollInfo.self, + from: """ + { + \"token\":\"test\", + \"active_users\":1, + \"read\":true, + \"write\":true, + \"upload\":true + } + """.data(using: .utf8)! + ), + failedToParseBody: false + ) + ), + try! JSONEncoder().encode( + OpenGroupAPI.BatchSubResponse( + code: 200, + headers: [:], + body: [OpenGroupAPI.Message](), + failedToParseBody: false + ) + ), + try! JSONEncoder().encode( + OpenGroupAPI.BatchSubResponse( + code: 200, + headers: [:], + body: [OpenGroupAPI.DirectMessage](), + failedToParseBody: false + ) + ), + try! JSONEncoder().encode( + OpenGroupAPI.BatchSubResponse( + code: 200, + headers: [:], + body: [OpenGroupAPI.DirectMessage](), + failedToParseBody: false + ) ) - ), - try! JSONEncoder().encode( - OpenGroupAPI.BatchSubResponse( - code: 200, - headers: [:], - body: try! JSONDecoder().decode( - OpenGroupAPI.RoomPollInfo.self, - from: """ - { - \"token\":\"test\", - \"active_users\":1, - \"read\":true, - \"write\":true, - \"upload\":true - } - """.data(using: .utf8)! - ), - failedToParseBody: false - ) - ), - try! JSONEncoder().encode( - OpenGroupAPI.BatchSubResponse( - code: 200, - headers: [:], - body: [OpenGroupAPI.Message](), - failedToParseBody: false - ) - ), - try! JSONEncoder().encode( - OpenGroupAPI.BatchSubResponse( - code: 200, - headers: [:], - body: [OpenGroupAPI.DirectMessage](), - failedToParseBody: false - ) - ), - try! JSONEncoder().encode( - OpenGroupAPI.BatchSubResponse( - code: 200, - headers: [:], - body: [OpenGroupAPI.DirectMessage](), - failedToParseBody: false - ) - ) - ] - - return "[\(responses.map { String(data: $0, encoding: .utf8)! }.joined(separator: ","))]".data(using: .utf8) + ] + + return "[\(responses.map { String(data: $0, encoding: .utf8)! }.joined(separator: ","))]".data(using: .utf8) + } } + + dependencies = dependencies.with(api: LocalTestApi.self) + } + + it("generates the correct request") { + OpenGroupAPI.poll("testServer", hasPerformedInitialPoll: false, timeSinceLastPoll: 0, using: dependencies) + .get { result in pollResponse = result } + .catch { requestError in error = requestError } + .retainUntilComplete() + + expect(pollResponse) + .toEventuallyNot( + beNil(), + timeout: .milliseconds(100) + ) + expect(error?.localizedDescription).to(beNil()) + + // Validate the response data + expect(pollResponse?.values).to(haveCount(5)) + expect(pollResponse?.keys).to(contain(.capabilities)) + expect(pollResponse?.keys).to(contain(.roomPollInfo("testRoom", 0))) + expect(pollResponse?.keys).to(contain(.roomMessagesRecent("testRoom"))) + expect(pollResponse?.keys).to(contain(.inbox)) + expect(pollResponse?.keys).to(contain(.outbox)) + expect(pollResponse?[.capabilities]?.0).to(beAKindOf(TestResponseInfo.self)) + + // Validate request data + let requestData: TestApi.RequestData? = (pollResponse?[.capabilities]?.0 as? TestResponseInfo)?.requestData + expect(requestData?.urlString).to(equal("testServer/batch")) + expect(requestData?.httpMethod).to(equal("POST")) + expect(requestData?.server).to(equal("testServer")) + expect(requestData?.publicKey).to(equal("7aecdcade88d881d2327ab011afd2e04c2ec6acffc9e9df45aaf78a151bd2f7d")) + } + + it("retrieves recent messages if there was no last message") { + OpenGroupAPI + .poll( + "testServer", + hasPerformedInitialPoll: false, + timeSinceLastPoll: 0, + using: dependencies + ) + .get { result in pollResponse = result } + .catch { requestError in error = requestError } + .retainUntilComplete() + + expect(pollResponse) + .toEventuallyNot( + beNil(), + timeout: .milliseconds(100) + ) + expect(error?.localizedDescription).to(beNil()) + expect(pollResponse?.keys).to(contain(.roomMessagesRecent("testRoom"))) + } + + it("retrieves recent messages if there was a last message and it has not performed the initial poll and the last message was too long ago") { + testStorage.mockData[.openGroupSequenceNumber] = ["testServer.testRoom": Int64(123)] + + OpenGroupAPI + .poll( + "testServer", + hasPerformedInitialPoll: false, + timeSinceLastPoll: (OpenGroupAPI.Poller.maxInactivityPeriod + 1), + using: dependencies + ) + .get { result in pollResponse = result } + .catch { requestError in error = requestError } + .retainUntilComplete() + + expect(pollResponse) + .toEventuallyNot( + beNil(), + timeout: .milliseconds(100) + ) + expect(error?.localizedDescription).to(beNil()) + expect(pollResponse?.keys).to(contain(.roomMessagesRecent("testRoom"))) + } + + it("retrieves recent messages if there was a last message and it has performed an initial poll but it was not too long ago") { + testStorage.mockData[.openGroupSequenceNumber] = ["testServer.testRoom": Int64(123)] + + OpenGroupAPI + .poll( + "testServer", + hasPerformedInitialPoll: false, + timeSinceLastPoll: 0, + using: dependencies + ) + .get { result in pollResponse = result } + .catch { requestError in error = requestError } + .retainUntilComplete() + + expect(pollResponse) + .toEventuallyNot( + beNil(), + timeout: .milliseconds(100) + ) + expect(error?.localizedDescription).to(beNil()) + expect(pollResponse?.keys).to(contain(.roomMessagesSince("testRoom", seqNo: 123))) + } + + it("retrieves recent messages if there was a last message and there has already been a poll this session") { + testStorage.mockData[.openGroupSequenceNumber] = ["testServer.testRoom": Int64(123)] + + OpenGroupAPI + .poll( + "testServer", + hasPerformedInitialPoll: true, + timeSinceLastPoll: 0, + using: dependencies + ) + .get { result in pollResponse = result } + .catch { requestError in error = requestError } + .retainUntilComplete() + + expect(pollResponse) + .toEventuallyNot( + beNil(), + timeout: .milliseconds(100) + ) + expect(error?.localizedDescription).to(beNil()) + expect(pollResponse?.keys).to(contain(.roomMessagesSince("testRoom", seqNo: 123))) + } + + it("retrieves recent inbox messages if there was no last message") { + OpenGroupAPI + .poll( + "testServer", + hasPerformedInitialPoll: true, + timeSinceLastPoll: 0, + using: dependencies + ) + .get { result in pollResponse = result } + .catch { requestError in error = requestError } + .retainUntilComplete() + + expect(pollResponse) + .toEventuallyNot( + beNil(), + timeout: .milliseconds(100) + ) + expect(error?.localizedDescription).to(beNil()) + expect(pollResponse?.keys).to(contain(.inbox)) + } + + it("retrieves inbox messages since the last message if there was one") { + testStorage.mockData[.openGroupInboxLatestMessageId] = ["testServer": Int64(124)] + + OpenGroupAPI + .poll( + "testServer", + hasPerformedInitialPoll: true, + timeSinceLastPoll: 0, + using: dependencies + ) + .get { result in pollResponse = result } + .catch { requestError in error = requestError } + .retainUntilComplete() + + expect(pollResponse) + .toEventuallyNot( + beNil(), + timeout: .milliseconds(100) + ) + expect(error?.localizedDescription).to(beNil()) + expect(pollResponse?.keys).to(contain(.inboxSince(id: 124))) + } + + it("retrieves recent outbox messages if there was no last message") { + OpenGroupAPI + .poll( + "testServer", + hasPerformedInitialPoll: true, + timeSinceLastPoll: 0, + using: dependencies + ) + .get { result in pollResponse = result } + .catch { requestError in error = requestError } + .retainUntilComplete() + + expect(pollResponse) + .toEventuallyNot( + beNil(), + timeout: .milliseconds(100) + ) + expect(error?.localizedDescription).to(beNil()) + expect(pollResponse?.keys).to(contain(.outbox)) + } + + it("retrieves outbox messages since the last message if there was one") { + testStorage.mockData[.openGroupOutboxLatestMessageId] = ["testServer": Int64(125)] + + OpenGroupAPI + .poll( + "testServer", + hasPerformedInitialPoll: true, + timeSinceLastPoll: 0, + using: dependencies + ) + .get { result in pollResponse = result } + .catch { requestError in error = requestError } + .retainUntilComplete() + + expect(pollResponse) + .toEventuallyNot( + beNil(), + timeout: .milliseconds(100) + ) + expect(error?.localizedDescription).to(beNil()) + expect(pollResponse?.keys).to(contain(.outboxSince(id: 125))) + } + } + + context("and given an invalid response") { + it("does not update the poll state") { + testStorage.mockData[.openGroupSequenceNumber] = ["testServer.testRoom": Int64(123)] + + OpenGroupAPI.poll("testServer", hasPerformedInitialPoll: false, timeSinceLastPoll: 0, using: dependencies) + .get { result in pollResponse = result } + .catch { requestError in error = requestError } + .retainUntilComplete() + + expect(error?.localizedDescription) + .toEventually( + equal(HTTP.Error.invalidResponse.localizedDescription), + timeout: .milliseconds(100) + ) + + expect(pollResponse).to(beNil()) + expect(testUserDefaults[.lastOpen]).to(beNil()) + } + + it("errors when no data is returned") { + OpenGroupAPI.poll("testServer", hasPerformedInitialPoll: false, timeSinceLastPoll: 0, using: dependencies) + .get { result in pollResponse = result } + .catch { requestError in error = requestError } + .retainUntilComplete() + + expect(error?.localizedDescription) + .toEventually( + equal(HTTP.Error.parsingFailed.localizedDescription), + timeout: .milliseconds(100) + ) + + expect(pollResponse).to(beNil()) + } + + it("errors when invalid data is returned") { + class LocalTestApi: TestApi { + override class var mockResponse: Data? { return Data() } + } + dependencies = dependencies.with(api: LocalTestApi.self) + + OpenGroupAPI.poll("testServer", hasPerformedInitialPoll: false, timeSinceLastPoll: 0, using: dependencies) + .get { result in pollResponse = result } + .catch { requestError in error = requestError } + .retainUntilComplete() + + expect(error?.localizedDescription) + .toEventually( + equal(HTTP.Error.parsingFailed.localizedDescription), + timeout: .milliseconds(100) + ) + + expect(pollResponse).to(beNil()) + } + + it("errors when an empty array is returned") { + class LocalTestApi: TestApi { + override class var mockResponse: Data? { return "[]".data(using: .utf8) } + } + dependencies = dependencies.with(api: LocalTestApi.self) + + OpenGroupAPI.poll("testServer", hasPerformedInitialPoll: false, timeSinceLastPoll: 0, using: dependencies) + .get { result in pollResponse = result } + .catch { requestError in error = requestError } + .retainUntilComplete() + + expect(error?.localizedDescription) + .toEventually( + equal(HTTP.Error.parsingFailed.localizedDescription), + timeout: .milliseconds(100) + ) + + expect(pollResponse).to(beNil()) + } + + it("errors when an empty object is returned") { + class LocalTestApi: TestApi { + override class var mockResponse: Data? { return "{}".data(using: .utf8) } + } + dependencies = dependencies.with(api: LocalTestApi.self) + + OpenGroupAPI.poll("testServer", hasPerformedInitialPoll: false, timeSinceLastPoll: 0, using: dependencies) + .get { result in pollResponse = result } + .catch { requestError in error = requestError } + .retainUntilComplete() + + expect(error?.localizedDescription) + .toEventually( + equal(HTTP.Error.parsingFailed.localizedDescription), + timeout: .milliseconds(100) + ) + + expect(pollResponse).to(beNil()) + } + + it("errors when a different number of responses are returned") { + class LocalTestApi: TestApi { + override class var mockResponse: Data? { + let responses: [Data] = [ + try! JSONEncoder().encode( + OpenGroupAPI.BatchSubResponse( + code: 200, + headers: [:], + body: OpenGroupAPI.Capabilities(capabilities: [], missing: nil), + failedToParseBody: false + ) + ), + try! JSONEncoder().encode( + OpenGroupAPI.BatchSubResponse( + code: 200, + headers: [:], + body: try! JSONDecoder().decode( + OpenGroupAPI.RoomPollInfo.self, + from: """ + { + \"token\":\"test\", + \"active_users\":1, + \"read\":true, + \"write\":true, + \"upload\":true + } + """.data(using: .utf8)! + ), + failedToParseBody: false + ) + ) + ] + + return "[\(responses.map { String(data: $0, encoding: .utf8)! }.joined(separator: ","))]".data(using: .utf8) + } + } + dependencies = dependencies.with(api: LocalTestApi.self) + + OpenGroupAPI.poll("testServer", hasPerformedInitialPoll: false, timeSinceLastPoll: 0, using: dependencies) + .get { result in pollResponse = result } + .catch { requestError in error = requestError } + .retainUntilComplete() + + expect(error?.localizedDescription) + .toEventually( + equal(HTTP.Error.parsingFailed.localizedDescription), + timeout: .milliseconds(100) + ) + + expect(pollResponse).to(beNil()) + } + + it("errors when an unexpected response is returned") { + class LocalTestApi: TestApi { + override class var mockResponse: Data? { + let responses: [Data] = [ + try! JSONEncoder().encode( + OpenGroupAPI.BatchSubResponse( + code: 200, + headers: [:], + body: OpenGroupAPI.PinnedMessage(id: 1, pinnedAt: 1, pinnedBy: ""), + failedToParseBody: false + ) + ), + try! JSONEncoder().encode( + OpenGroupAPI.BatchSubResponse( + code: 200, + headers: [:], + body: OpenGroupAPI.PinnedMessage(id: 1, pinnedAt: 1, pinnedBy: ""), + failedToParseBody: false + ) + ), + try! JSONEncoder().encode( + OpenGroupAPI.BatchSubResponse( + code: 200, + headers: [:], + body: OpenGroupAPI.PinnedMessage(id: 1, pinnedAt: 1, pinnedBy: ""), + failedToParseBody: false + ) + ) + ] + + return "[\(responses.map { String(data: $0, encoding: .utf8)! }.joined(separator: ","))]".data(using: .utf8) + } + } + dependencies = dependencies.with(api: LocalTestApi.self) + + OpenGroupAPI.poll("testServer", hasPerformedInitialPoll: false, timeSinceLastPoll: 0, using: dependencies) + .get { result in pollResponse = result } + .catch { requestError in error = requestError } + .retainUntilComplete() + + expect(error?.localizedDescription) + .toEventually( + equal(HTTP.Error.parsingFailed.localizedDescription), + timeout: .milliseconds(100) + ) + + expect(pollResponse).to(beNil()) + } + } + } + + // MARK: - Capabilities + + context("when doing a capabilities request") { + it("generates the request and handles the response correctly") { + class LocalTestApi: TestApi { + static let data: OpenGroupAPI.Capabilities = OpenGroupAPI.Capabilities(capabilities: [.sogs], missing: nil) + + override class var mockResponse: Data? { try! JSONEncoder().encode(data) } } dependencies = dependencies.with(api: LocalTestApi.self) - OpenGroupAPI.poll("testServer", using: dependencies) - .get { result in pollResponse = result } + var response: (info: OnionRequestResponseInfoType, data: OpenGroupAPI.Capabilities)? + + OpenGroupAPI.capabilities(on: "testServer", using: dependencies) + .get { result in response = result } .catch { requestError in error = requestError } .retainUntilComplete() - expect(pollResponse) + expect(response) .toEventuallyNot( beNil(), timeout: .milliseconds(100) @@ -229,196 +660,346 @@ class OpenGroupAPISpec: QuickSpec { expect(error?.localizedDescription).to(beNil()) // Validate the response data - expect(pollResponse?.values).to(haveCount(5)) - expect(pollResponse?.keys).to(contain(.capabilities)) - expect(pollResponse?.keys).to(contain(.roomPollInfo("testRoom", 0))) - expect(pollResponse?.keys).to(contain(.roomMessagesRecent("testRoom"))) - expect(pollResponse?.keys).to(contain(.inbox)) - expect(pollResponse?.keys).to(contain(.outbox)) - expect(pollResponse?[.capabilities]?.0).to(beAKindOf(TestResponseInfo.self)) + expect(response?.data).to(equal(LocalTestApi.data)) // Validate request data - let requestData: TestApi.RequestData? = (pollResponse?[.capabilities]?.0 as? TestResponseInfo)?.requestData - expect(requestData?.urlString).to(equal("testServer/batch")) - expect(requestData?.httpMethod).to(equal("POST")) + let requestData: TestApi.RequestData? = (response?.info as? TestResponseInfo)?.requestData + expect(requestData?.httpMethod).to(equal("GET")) expect(requestData?.server).to(equal("testServer")) - expect(requestData?.publicKey).to(equal("7aecdcade88d881d2327ab011afd2e04c2ec6acffc9e9df45aaf78a151bd2f7d")) + expect(requestData?.urlString).to(equal("testServer/capabilities")) } - - it("errors when no data is returned") { - OpenGroupAPI.poll("testServer", using: dependencies) - .get { result in pollResponse = result } - .catch { requestError in error = requestError } - .retainUntilComplete() - - expect(error?.localizedDescription) - .toEventually( - equal(HTTP.Error.parsingFailed.localizedDescription), - timeout: .milliseconds(100) - ) - - expect(pollResponse).to(beNil()) - } - - it("errors when invalid data is returned") { + } + + // MARK: - Rooms + + context("when doing a rooms request") { + it("generates the request and handles the response correctly") { class LocalTestApi: TestApi { - override class var mockResponse: Data? { return Data() } + static let data: [OpenGroupAPI.Room] = [ + OpenGroupAPI.Room( + token: "test", + name: "test", + description: nil, + infoUpdates: 0, + messageSequence: 0, + created: 0, + activeUsers: 0, + activeUsersCutoff: 0, + imageId: nil, + pinnedMessages: nil, + admin: false, + globalAdmin: false, + admins: [], + hiddenAdmins: nil, + moderator: false, + globalModerator: false, + moderators: [], + hiddenModerators: nil, + read: false, + defaultRead: nil, + defaultAccessible: nil, + write: false, + defaultWrite: nil, + upload: false, + defaultUpload: nil + ) + ] + + override class var mockResponse: Data? { return try! JSONEncoder().encode(data) } } dependencies = dependencies.with(api: LocalTestApi.self) - OpenGroupAPI.poll("testServer", using: dependencies) - .get { result in pollResponse = result } + var response: (info: OnionRequestResponseInfoType, data: [OpenGroupAPI.Room])? + + OpenGroupAPI.rooms(for: "testServer", using: dependencies) + .get { result in response = result } .catch { requestError in error = requestError } .retainUntilComplete() - expect(error?.localizedDescription) - .toEventually( - equal(HTTP.Error.parsingFailed.localizedDescription), + expect(response) + .toEventuallyNot( + beNil(), timeout: .milliseconds(100) ) + expect(error?.localizedDescription).to(beNil()) - expect(pollResponse).to(beNil()) + // Validate the response data + expect(response?.data).to(equal(LocalTestApi.data)) + + // Validate request data + let requestData: TestApi.RequestData? = (response?.info as? TestResponseInfo)?.requestData + expect(requestData?.httpMethod).to(equal("GET")) + expect(requestData?.server).to(equal("testServer")) + expect(requestData?.urlString).to(equal("testServer/rooms")) } - - it("errors when an empty array is returned") { - class LocalTestApi: TestApi { - override class var mockResponse: Data? { return "[]".data(using: .utf8) } - } - dependencies = dependencies.with(api: LocalTestApi.self) - - OpenGroupAPI.poll("testServer", using: dependencies) - .get { result in pollResponse = result } - .catch { requestError in error = requestError } - .retainUntilComplete() - - expect(error?.localizedDescription) - .toEventually( - equal(HTTP.Error.parsingFailed.localizedDescription), - timeout: .milliseconds(100) - ) - - expect(pollResponse).to(beNil()) - } - - it("errors when an empty object is returned") { - class LocalTestApi: TestApi { - override class var mockResponse: Data? { return "{}".data(using: .utf8) } - } - dependencies = dependencies.with(api: LocalTestApi.self) - - OpenGroupAPI.poll("testServer", using: dependencies) - .get { result in pollResponse = result } - .catch { requestError in error = requestError } - .retainUntilComplete() - - expect(error?.localizedDescription) - .toEventually( - equal(HTTP.Error.parsingFailed.localizedDescription), - timeout: .milliseconds(100) - ) - - expect(pollResponse).to(beNil()) - } - - it("errors when a different number of responses are returned") { - class LocalTestApi: TestApi { - override class var mockResponse: Data? { - let responses: [Data] = [ - try! JSONEncoder().encode( - OpenGroupAPI.BatchSubResponse( - code: 200, - headers: [:], - body: OpenGroupAPI.Capabilities(capabilities: [], missing: nil), - failedToParseBody: false - ) - ), - try! JSONEncoder().encode( - OpenGroupAPI.BatchSubResponse( - code: 200, - headers: [:], - body: try! JSONDecoder().decode( - OpenGroupAPI.RoomPollInfo.self, - from: """ - { - \"token\":\"test\", - \"active_users\":1, - \"read\":true, - \"write\":true, - \"upload\":true - } - """.data(using: .utf8)! - ), - failedToParseBody: false - ) - ) - ] + } + + // MARK: - CapabilitiesAndRoom + + context("when doing a capabilitiesAndRoom request") { + context("and given a correct response") { + it("generates the request and handles the response correctly") { + class LocalTestApi: TestApi { + static let capabilitiesData: OpenGroupAPI.Capabilities = OpenGroupAPI.Capabilities(capabilities: [.sogs], missing: nil) + static let roomData: OpenGroupAPI.Room = OpenGroupAPI.Room( + token: "test", + name: "test", + description: nil, + infoUpdates: 0, + messageSequence: 0, + created: 0, + activeUsers: 0, + activeUsersCutoff: 0, + imageId: nil, + pinnedMessages: nil, + admin: false, + globalAdmin: false, + admins: [], + hiddenAdmins: nil, + moderator: false, + globalModerator: false, + moderators: [], + hiddenModerators: nil, + read: false, + defaultRead: nil, + defaultAccessible: nil, + write: false, + defaultWrite: nil, + upload: false, + defaultUpload: nil + ) - return "[\(responses.map { String(data: $0, encoding: .utf8)! }.joined(separator: ","))]".data(using: .utf8) + override class var mockResponse: Data? { + let responses: [Data] = [ + try! JSONEncoder().encode( + OpenGroupAPI.BatchSubResponse( + code: 200, + headers: [:], + body: capabilitiesData, + failedToParseBody: false + ) + ), + try! JSONEncoder().encode( + OpenGroupAPI.BatchSubResponse( + code: 200, + headers: [:], + body: roomData, + failedToParseBody: false + ) + ) + ] + + return "[\(responses.map { String(data: $0, encoding: .utf8)! }.joined(separator: ","))]".data(using: .utf8) + } } + dependencies = dependencies.with(api: LocalTestApi.self) + + var response: (capabilities: (info: OnionRequestResponseInfoType, data: OpenGroupAPI.Capabilities?), room: (info: OnionRequestResponseInfoType, data: OpenGroupAPI.Room?))? + + OpenGroupAPI.capabilitiesAndRoom(for: "testRoom", on: "testServer", using: dependencies) + .get { result in response = result } + .catch { requestError in error = requestError } + .retainUntilComplete() + + expect(response) + .toEventuallyNot( + beNil(), + timeout: .milliseconds(100) + ) + expect(error?.localizedDescription).to(beNil()) + + // Validate the response data + expect(response?.capabilities.data).to(equal(LocalTestApi.capabilitiesData)) + expect(response?.room.data).to(equal(LocalTestApi.roomData)) + + // Validate request data + let requestData: TestApi.RequestData? = (response?.capabilities.info as? TestResponseInfo)?.requestData + expect(requestData?.httpMethod).to(equal("POST")) + expect(requestData?.server).to(equal("testServer")) + expect(requestData?.urlString).to(equal("testServer/sequence")) } - dependencies = dependencies.with(api: LocalTestApi.self) - - OpenGroupAPI.poll("testServer", using: dependencies) - .get { result in pollResponse = result } - .catch { requestError in error = requestError } - .retainUntilComplete() - - expect(error?.localizedDescription) - .toEventually( - equal(HTTP.Error.parsingFailed.localizedDescription), - timeout: .milliseconds(100) - ) - - expect(pollResponse).to(beNil()) } - it("errors when an unexpected response is returned") { - class LocalTestApi: TestApi { - override class var mockResponse: Data? { - let responses: [Data] = [ - try! JSONEncoder().encode( - OpenGroupAPI.BatchSubResponse( - code: 200, - headers: [:], - body: OpenGroupAPI.PinnedMessage(id: 1, pinnedAt: 1, pinnedBy: ""), - failedToParseBody: false - ) - ), - try! JSONEncoder().encode( - OpenGroupAPI.BatchSubResponse( - code: 200, - headers: [:], - body: OpenGroupAPI.PinnedMessage(id: 1, pinnedAt: 1, pinnedBy: ""), - failedToParseBody: false - ) - ), - try! JSONEncoder().encode( - OpenGroupAPI.BatchSubResponse( - code: 200, - headers: [:], - body: OpenGroupAPI.PinnedMessage(id: 1, pinnedAt: 1, pinnedBy: ""), - failedToParseBody: false - ) - ) - ] + context("and given an invalid response") { + it("errors when only a capabilities response is returned") { + class LocalTestApi: TestApi { + static let capabilitiesData: OpenGroupAPI.Capabilities = OpenGroupAPI.Capabilities(capabilities: [.sogs], missing: nil) - return "[\(responses.map { String(data: $0, encoding: .utf8)! }.joined(separator: ","))]".data(using: .utf8) + override class var mockResponse: Data? { + let responses: [Data] = [ + try! JSONEncoder().encode( + OpenGroupAPI.BatchSubResponse( + code: 200, + headers: [:], + body: capabilitiesData, + failedToParseBody: false + ) + ) + ] + + return "[\(responses.map { String(data: $0, encoding: .utf8)! }.joined(separator: ","))]".data(using: .utf8) + } } + dependencies = dependencies.with(api: LocalTestApi.self) + + var response: (capabilities: (info: OnionRequestResponseInfoType, data: OpenGroupAPI.Capabilities?), room: (info: OnionRequestResponseInfoType, data: OpenGroupAPI.Room?))? + + OpenGroupAPI.capabilitiesAndRoom(for: "testRoom", on: "testServer", using: dependencies) + .get { result in response = result } + .catch { requestError in error = requestError } + .retainUntilComplete() + + expect(error?.localizedDescription) + .toEventually( + equal(HTTP.Error.parsingFailed.localizedDescription), + timeout: .milliseconds(100) + ) + + expect(response).to(beNil()) } - dependencies = dependencies.with(api: LocalTestApi.self) - OpenGroupAPI.poll("testServer", using: dependencies) - .get { result in pollResponse = result } - .catch { requestError in error = requestError } - .retainUntilComplete() + it("errors when only a room response is returned") { + class LocalTestApi: TestApi { + static let roomData: OpenGroupAPI.Room = OpenGroupAPI.Room( + token: "test", + name: "test", + description: nil, + infoUpdates: 0, + messageSequence: 0, + created: 0, + activeUsers: 0, + activeUsersCutoff: 0, + imageId: nil, + pinnedMessages: nil, + admin: false, + globalAdmin: false, + admins: [], + hiddenAdmins: nil, + moderator: false, + globalModerator: false, + moderators: [], + hiddenModerators: nil, + read: false, + defaultRead: nil, + defaultAccessible: nil, + write: false, + defaultWrite: nil, + upload: false, + defaultUpload: nil + ) + + override class var mockResponse: Data? { + let responses: [Data] = [ + try! JSONEncoder().encode( + OpenGroupAPI.BatchSubResponse( + code: 200, + headers: [:], + body: roomData, + failedToParseBody: false + ) + ) + ] + + return "[\(responses.map { String(data: $0, encoding: .utf8)! }.joined(separator: ","))]".data(using: .utf8) + } + } + dependencies = dependencies.with(api: LocalTestApi.self) + + var response: (capabilities: (info: OnionRequestResponseInfoType, data: OpenGroupAPI.Capabilities?), room: (info: OnionRequestResponseInfoType, data: OpenGroupAPI.Room?))? + + OpenGroupAPI.capabilitiesAndRoom(for: "testRoom", on: "testServer", using: dependencies) + .get { result in response = result } + .catch { requestError in error = requestError } + .retainUntilComplete() + + expect(error?.localizedDescription) + .toEventually( + equal(HTTP.Error.parsingFailed.localizedDescription), + timeout: .milliseconds(100) + ) + + expect(response).to(beNil()) + } - expect(error?.localizedDescription) - .toEventually( - equal(HTTP.Error.parsingFailed.localizedDescription), - timeout: .milliseconds(100) - ) - - expect(pollResponse).to(beNil()) + it("errors when an extra response is returned") { + class LocalTestApi: TestApi { + static let capabilitiesData: OpenGroupAPI.Capabilities = OpenGroupAPI.Capabilities(capabilities: [.sogs], missing: nil) + static let roomData: OpenGroupAPI.Room = OpenGroupAPI.Room( + token: "test", + name: "test", + description: nil, + infoUpdates: 0, + messageSequence: 0, + created: 0, + activeUsers: 0, + activeUsersCutoff: 0, + imageId: nil, + pinnedMessages: nil, + admin: false, + globalAdmin: false, + admins: [], + hiddenAdmins: nil, + moderator: false, + globalModerator: false, + moderators: [], + hiddenModerators: nil, + read: false, + defaultRead: nil, + defaultAccessible: nil, + write: false, + defaultWrite: nil, + upload: false, + defaultUpload: nil + ) + + override class var mockResponse: Data? { + let responses: [Data] = [ + try! JSONEncoder().encode( + OpenGroupAPI.BatchSubResponse( + code: 200, + headers: [:], + body: capabilitiesData, + failedToParseBody: false + ) + ), + try! JSONEncoder().encode( + OpenGroupAPI.BatchSubResponse( + code: 200, + headers: [:], + body: roomData, + failedToParseBody: false + ) + ), + try! JSONEncoder().encode( + OpenGroupAPI.BatchSubResponse( + code: 200, + headers: [:], + body: OpenGroupAPI.PinnedMessage(id: 1, pinnedAt: 1, pinnedBy: ""), + failedToParseBody: false + ) + ) + ] + + return "[\(responses.map { String(data: $0, encoding: .utf8)! }.joined(separator: ","))]".data(using: .utf8) + } + } + dependencies = dependencies.with(api: LocalTestApi.self) + + var response: (capabilities: (info: OnionRequestResponseInfoType, data: OpenGroupAPI.Capabilities?), room: (info: OnionRequestResponseInfoType, data: OpenGroupAPI.Room?))? + + OpenGroupAPI.capabilitiesAndRoom(for: "testRoom", on: "testServer", using: dependencies) + .get { result in response = result } + .catch { requestError in error = requestError } + .retainUntilComplete() + + expect(error?.localizedDescription) + .toEventually( + equal(HTTP.Error.parsingFailed.localizedDescription), + timeout: .milliseconds(100) + ) + + expect(response).to(beNil()) + } } } diff --git a/SessionMessagingKitTests/_TestUtilities/TestUserDefaults.swift b/SessionMessagingKitTests/_TestUtilities/TestUserDefaults.swift new file mode 100644 index 000000000..a01b5eda2 --- /dev/null +++ b/SessionMessagingKitTests/_TestUtilities/TestUserDefaults.swift @@ -0,0 +1,27 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation +import SessionUtilitiesKit + +class TestUserDefaults: UserDefaultsType { + var storage: [String: Any] = [:] + + func object(forKey defaultName: String) -> Any? { return storage[defaultName] } + func string(forKey defaultName: String) -> String? { return storage[defaultName] as? String } + func array(forKey defaultName: String) -> [Any]? { return storage[defaultName] as? [Any] } + func dictionary(forKey defaultName: String) -> [String: Any]? { return storage[defaultName] as? [String: Any] } + func data(forKey defaultName: String) -> Data? { return storage[defaultName] as? Data } + func stringArray(forKey defaultName: String) -> [String]? { return storage[defaultName] as? [String] } + func integer(forKey defaultName: String) -> Int { return ((storage[defaultName] as? Int) ?? 0) } + func float(forKey defaultName: String) -> Float { return ((storage[defaultName] as? Float) ?? 0) } + func double(forKey defaultName: String) -> Double { return ((storage[defaultName] as? Double) ?? 0) } + func bool(forKey defaultName: String) -> Bool { return ((storage[defaultName] as? Bool) ?? false) } + func url(forKey defaultName: String) -> URL? { return storage[defaultName] as? URL } + + func set(_ value: Any?, forKey defaultName: String) { storage[defaultName] = value } + func set(_ value: Int, forKey defaultName: String) { storage[defaultName] = value } + func set(_ value: Float, forKey defaultName: String) { storage[defaultName] = value } + func set(_ value: Double, forKey defaultName: String) { storage[defaultName] = value } + func set(_ value: Bool, forKey defaultName: String) { storage[defaultName] = value } + func set(_ url: URL?, forKey defaultName: String) { storage[defaultName] = url } +} diff --git a/SessionSnodeKit/OnionRequestAPI.swift b/SessionSnodeKit/OnionRequestAPI.swift index 1d4128457..6c7a4dd52 100644 --- a/SessionSnodeKit/OnionRequestAPI.swift +++ b/SessionSnodeKit/OnionRequestAPI.swift @@ -308,9 +308,6 @@ public enum OnionRequestAPI: OnionRequestAPIType { /// Sends an onion request to `server`. Builds new paths as needed. public static func sendOnionRequest(_ request: URLRequest, to server: String, using version: Version = .v4, with x25519PublicKey: String) -> Promise<(OnionRequestResponseInfoType, Data?)> { - guard version != .v4 || server == "https://chat.lokinet.dev" else { // TODO: Remove this - return sendOnionRequest(request, to: server, using: .v3, with: x25519PublicKey) - } guard let url = request.url, let host = request.url?.host else { return Promise(error: Error.invalidURL) } let scheme: String? = url.scheme diff --git a/SessionUtilitiesKit/General/SNUserDefaults.swift b/SessionUtilitiesKit/General/SNUserDefaults.swift index 0e27a2a3b..4799d5cc8 100644 --- a/SessionUtilitiesKit/General/SNUserDefaults.swift +++ b/SessionUtilitiesKit/General/SNUserDefaults.swift @@ -1,5 +1,28 @@ import Foundation +public protocol UserDefaultsType: AnyObject { + func object(forKey defaultName: String) -> Any? + func string(forKey defaultName: String) -> String? + func array(forKey defaultName: String) -> [Any]? + func dictionary(forKey defaultName: String) -> [String : Any]? + func data(forKey defaultName: String) -> Data? + func stringArray(forKey defaultName: String) -> [String]? + func integer(forKey defaultName: String) -> Int + func float(forKey defaultName: String) -> Float + func double(forKey defaultName: String) -> Double + func bool(forKey defaultName: String) -> Bool + func url(forKey defaultName: String) -> URL? + + func set(_ value: Any?, forKey defaultName: String) + func set(_ value: Int, forKey defaultName: String) + func set(_ value: Float, forKey defaultName: String) + func set(_ value: Double, forKey defaultName: String) + func set(_ value: Bool, forKey defaultName: String) + func set(_ url: URL?, forKey defaultName: String) +} + +extension UserDefaults: UserDefaultsType {} + public enum SNUserDefaults { public enum Bool : Swift.String { @@ -31,7 +54,7 @@ public enum SNUserDefaults { } } -public extension UserDefaults { +public extension UserDefaultsType { subscript(bool: SNUserDefaults.Bool) -> Bool { get { return self.bool(forKey: bool.rawValue) }