From 9e471fb903c132a84cfde08a020b1b7e31599831 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Mon, 2 Oct 2023 10:49:15 +1100 Subject: [PATCH] Cleaned up some TODOs and refactored more requests to prepared ones --- Session.xcodeproj/project.pbxproj | 8 + Session/Closed Groups/EditClosedGroupVC.swift | 61 +++--- Session/Settings/NukeDataModal.swift | 75 ++++---- .../MessageSender+Groups.swift | 10 +- SessionShareExtension/ThreadPickerVC.swift | 29 +-- .../Models/DeleteAllBeforeRequest.swift | 13 +- .../Models/DeleteAllMessagesRequest.swift | 12 +- SessionSnodeKit/Models/SnodeRequest.swift | 11 ++ .../PreparedRequest+OnionRequest.swift | 50 ++++- .../Networking/Request+SnodeAPI.swift | 83 ++++++++- .../Networking/ResponseInfo+SnodeAPI.swift | 18 ++ SessionSnodeKit/Networking/SnodeAPI.swift | 176 +++++++++--------- .../Types/UpdatableTimestamp.swift | 7 + .../Networking/ResponseInfo.swift | 1 - 14 files changed, 390 insertions(+), 164 deletions(-) create mode 100644 SessionSnodeKit/Networking/ResponseInfo+SnodeAPI.swift create mode 100644 SessionSnodeKit/Types/UpdatableTimestamp.swift diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index 48a632548..06552c7d4 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -510,6 +510,8 @@ FD17D7CD27F546FF00122BE0 /* Setting.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D7CC27F546FF00122BE0 /* Setting.swift */; }; FD17D7E527F6A09900122BE0 /* Identity.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D7E427F6A09900122BE0 /* Identity.swift */; }; FD17D7E727F6A16700122BE0 /* _003_YDBToGRDBMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D7E627F6A16700122BE0 /* _003_YDBToGRDBMigration.swift */; }; + FD19363A2ACA25BA004BCF0F /* UpdatableTimestamp.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD1936392ACA25BA004BCF0F /* UpdatableTimestamp.swift */; }; + FD19363C2ACA3134004BCF0F /* ResponseInfo+SnodeAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD19363B2ACA3134004BCF0F /* ResponseInfo+SnodeAPI.swift */; }; FD1A94FB2900D1C2000D73D3 /* PersistableRecord+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD1A94FA2900D1C2000D73D3 /* PersistableRecord+Utilities.swift */; }; FD1A94FE2900D2EA000D73D3 /* PersistableRecordUtilitiesSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD1A94FD2900D2EA000D73D3 /* PersistableRecordUtilitiesSpec.swift */; }; FD1C98E4282E3C5B00B76F9E /* UINavigationBar+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD1C98E3282E3C5B00B76F9E /* UINavigationBar+Utilities.swift */; }; @@ -1735,6 +1737,8 @@ FD17D7CC27F546FF00122BE0 /* Setting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Setting.swift; sourceTree = ""; }; FD17D7E427F6A09900122BE0 /* Identity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Identity.swift; sourceTree = ""; }; FD17D7E627F6A16700122BE0 /* _003_YDBToGRDBMigration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _003_YDBToGRDBMigration.swift; sourceTree = ""; }; + FD1936392ACA25BA004BCF0F /* UpdatableTimestamp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdatableTimestamp.swift; sourceTree = ""; }; + FD19363B2ACA3134004BCF0F /* ResponseInfo+SnodeAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ResponseInfo+SnodeAPI.swift"; sourceTree = ""; }; FD1A94FA2900D1C2000D73D3 /* PersistableRecord+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PersistableRecord+Utilities.swift"; sourceTree = ""; }; FD1A94FD2900D2EA000D73D3 /* PersistableRecordUtilitiesSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistableRecordUtilitiesSpec.swift; sourceTree = ""; }; FD1C98E3282E3C5B00B76F9E /* UINavigationBar+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UINavigationBar+Utilities.swift"; sourceTree = ""; }; @@ -4611,6 +4615,7 @@ FDF848E029405D6E007DCAE5 /* SnodeAPIError.swift */, FDF8489029405C13007DCAE5 /* SnodeAPINamespace.swift */, FD47E0BF2AA83D7300A55E41 /* SwarmDrainBehaviour.swift */, + FD1936392ACA25BA004BCF0F /* UpdatableTimestamp.swift */, FD43242F2999F0BC008A0213 /* ValidatableResponse.swift */, ); path = Types; @@ -4622,6 +4627,7 @@ FD47E0AA2AA68EEA00A55E41 /* Authentication.swift */, FDF8489329405C1B007DCAE5 /* SnodeAPI.swift */, FD47E0B42AA6D7AA00A55E41 /* Request+SnodeAPI.swift */, + FD19363B2ACA3134004BCF0F /* ResponseInfo+SnodeAPI.swift */, FDF848E829405E4E007DCAE5 /* OnionRequestAPI.swift */, FDF848E929405E4E007DCAE5 /* OnionRequestAPI+Encryption.swift */, FDF848EA29405E4E007DCAE5 /* Notification+OnionRequestAPI.swift */, @@ -6008,6 +6014,7 @@ FDF848BC29405C5A007DCAE5 /* SnodeRecursiveResponse.swift in Sources */, FDF848C029405C5A007DCAE5 /* ONSResolveResponse.swift in Sources */, FD17D7A427F40F8100122BE0 /* _003_YDBToGRDBMigration.swift in Sources */, + FD19363A2ACA25BA004BCF0F /* UpdatableTimestamp.swift in Sources */, FDFC4D9A29F0C51500992FB6 /* String+Trimming.swift in Sources */, FDB5DAF32A96DD4F002C8721 /* PreparedRequest+OnionRequest.swift in Sources */, FDF848C629405C5B007DCAE5 /* DeleteAllMessagesRequest.swift in Sources */, @@ -6017,6 +6024,7 @@ FDF848D129405C5B007DCAE5 /* SnodeSwarmItem.swift in Sources */, FDF848DD29405C5B007DCAE5 /* LegacySendMessageRequest.swift in Sources */, FDF848BD29405C5A007DCAE5 /* GetMessagesRequest.swift in Sources */, + FD19363C2ACA3134004BCF0F /* ResponseInfo+SnodeAPI.swift in Sources */, FDF848DB29405C5B007DCAE5 /* DeleteMessagesResponse.swift in Sources */, FDF848E629405D6E007DCAE5 /* OnionRequestAPIDestination.swift in Sources */, FD47E0AB2AA68EEA00A55E41 /* Authentication.swift in Sources */, diff --git a/Session/Closed Groups/EditClosedGroupVC.swift b/Session/Closed Groups/EditClosedGroupVC.swift index 92ac3d77b..0a13bc51a 100644 --- a/Session/Closed Groups/EditClosedGroupVC.swift +++ b/Session/Closed Groups/EditClosedGroupVC.swift @@ -427,7 +427,7 @@ final class EditClosedGroupVC: BaseVC, UITableViewDataSource, UITableViewDelegat navigationController?.pushViewController(userSelectionVC, animated: true, completion: nil) } - private func commitChanges() { + private func commitChanges(using dependencies: Dependencies = Dependencies()) { let popToConversationVC: ((EditClosedGroupVC?) -> ()) = { editVC in guard let viewControllers: [UIViewController] = editVC?.navigationController?.viewControllers, @@ -443,9 +443,9 @@ final class EditClosedGroupVC: BaseVC, UITableViewDataSource, UITableViewDelegat let threadId: String = self.threadId let updatedName: String = self.name let userPublicKey: String = self.userPublicKey - let updatedMemberIds: Set = self.membersAndZombies - .map { $0.profileId } - .asSet() + let updatedMembers: [(String, Profile?)] = self.membersAndZombies + .map { ($0.profileId, $0.profile) } + let updatedMemberIds: Set = updatedMembers.map { $0.0 }.asSet() guard updatedMemberIds != self.originalMembersAndZombieIds || updatedName != self.originalName else { return popToConversationVC(self) @@ -464,24 +464,43 @@ final class EditClosedGroupVC: BaseVC, UITableViewDataSource, UITableViewDelegat } ModalActivityIndicatorViewController.present(fromViewController: navigationController) { _ in - Dependencies()[singleton: .storage] - .writePublisher { db in - // If the user is no longer a member then leave the group - guard !updatedMemberIds.contains(userPublicKey) else { return } - - try MessageSender.leave( - db, - groupPublicKey: threadId, - deleteThread: true - ) - } - .flatMap { - MessageSender.update( - legacyGroupPublicKey: threadId, - with: updatedMemberIds, - name: updatedName + // If the user is no longer a member then leave the group + guard updatedMemberIds.contains(userPublicKey) else { + dependencies[singleton: .storage] + .writePublisher { db in + try MessageSender.leave( + db, + groupPublicKey: threadId, + deleteThread: true + ) + } + .subscribe(on: DispatchQueue.global(qos: .userInitiated)) + .receive(on: DispatchQueue.main) + .sinkUntilComplete( + receiveCompletion: { [weak self] result in + self?.dismiss(animated: true, completion: nil) // Dismiss the loader + + switch result { + case .finished: popToConversationVC(self) + case .failure(let error): + self?.showError( + title: "GROUP_UPDATE_ERROR_TITLE".localized(), + message: error.localizedDescription + ) + } + } ) - } + return + } + + // Otherwise update the group details + MessageSender + .updateGroup( + groupIdentityPublicKey: threadId, + name: updatedName, + displayPicture: nil, + members: updatedMembers + ) .subscribe(on: DispatchQueue.global(qos: .userInitiated)) .receive(on: DispatchQueue.main) .sinkUntilComplete( diff --git a/Session/Settings/NukeDataModal.swift b/Session/Settings/NukeDataModal.swift index adbcb3f7f..43fbcb21d 100644 --- a/Session/Settings/NukeDataModal.swift +++ b/Session/Settings/NukeDataModal.swift @@ -168,43 +168,54 @@ final class NukeDataModal: Modal { presentedViewController: UIViewController, using dependencies: Dependencies = Dependencies() ) { + typealias PreparedClearRequests = ( + deleteAll: HTTP.PreparedRequest<[String: Bool]>, + inboxRequestInfo: [HTTP.PreparedRequest] + ) + ModalActivityIndicatorViewController .present(fromViewController: presentedViewController, canCancel: false) { [weak self] _ in - Publishers - .MergeMany( - dependencies[singleton: .storage] - .read { db -> [(String, HTTP.PreparedRequest)] in - return try OpenGroup - .filter(OpenGroup.Columns.isActive == true) - .select(.server) - .distinct() - .asRequest(of: String.self) - .fetchSet(db) - .map { ($0, try OpenGroupAPI.preparedClearInbox(db, on: $0))} - } - .defaulting(to: []) - .compactMap { server, preparedRequest in - preparedRequest - .send(using: dependencies) - .map { _ in [server: true] } - .eraseToAnyPublisher() - } - ) - .collect() + dependencies[singleton: .storage] + .readPublisher { db -> PreparedClearRequests in + let authInfo: SnodeAPI.AuthenticationInfo = try SnodeAPI.AuthenticationInfo( + db, + threadId: getUserHexEncodedPublicKey(db, using: dependencies), + using: dependencies + ) + + return ( + try SnodeAPI.preparedDeleteAllMessages( + namespace: .all, + authInfo: authInfo, + using: dependencies + ), + try OpenGroup + .filter(OpenGroup.Columns.isActive == true) + .select(.server) + .distinct() + .asRequest(of: String.self) + .fetchSet(db) + .map { server in + try OpenGroupAPI.preparedClearInbox(db, on: server) + .map { _, _ in server } + } + ) + } .subscribe(on: DispatchQueue.global(qos: .userInitiated), using: dependencies) - .flatMap { results in - dependencies[singleton: .storage] - .readPublisher(using: dependencies) { db in - try SnodeAPI.AuthenticationInfo( - db, - threadId: getUserHexEncodedPublicKey(db, using: dependencies), - using: dependencies - ) - } - .flatMap { SnodeAPI.deleteAllMessages(namespace: .all, authInfo: $0) } - .map { results.reduce($0) { result, next in result.updated(with: next) } } + .flatMap { preparedRequests -> AnyPublisher<(HTTP.PreparedRequest<[String: Bool]>, [String]), Error> in + Publishers + .MergeMany(preparedRequests.inboxRequestInfo.map { $0.send(using: dependencies) }) + .collect() + .map { response in (preparedRequests.deleteAll, response.map { $0.1 }) } .eraseToAnyPublisher() } + .flatMap { preparedDeleteAllRequest, clearedServers in + preparedDeleteAllRequest + .send(using: dependencies) + .map { _, data in + clearedServers.reduce(into: data) { result, next in result[next] = true } + } + } .receive(on: DispatchQueue.main, using: dependencies) .sinkUntilComplete( receiveCompletion: { result in diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageSender+Groups.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageSender+Groups.swift index 8b9fcc5c7..cf8150b10 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageSender+Groups.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageSender+Groups.swift @@ -165,11 +165,17 @@ extension MessageSender { groupIdentityPublicKey: String, name: String, displayPicture: SignalAttachment?, + members: [(String, Profile?)], using dependencies: Dependencies = Dependencies() ) -> AnyPublisher { guard SessionId.Prefix(from: groupIdentityPublicKey) == .group else { - return Fail(error: MessageSenderError.invalidClosedGroupUpdate) - .eraseToAnyPublisher() + // FIXME: Fail with `MessageSenderError.invalidClosedGroupUpdate` once support for legacy groups is removed + return MessageSender.update( + legacyGroupPublicKey: groupIdentityPublicKey, + with: members.map { $0.0 }.asSet(), + name: name, + using: dependencies + ) } return dependencies[singleton: .storage] diff --git a/SessionShareExtension/ThreadPickerVC.swift b/SessionShareExtension/ThreadPickerVC.swift index 2ba3bd8a4..c3dcffed9 100644 --- a/SessionShareExtension/ThreadPickerVC.swift +++ b/SessionShareExtension/ThreadPickerVC.swift @@ -221,11 +221,17 @@ final class ThreadPickerVC: UIViewController, UITableViewDataSource, UITableView ) : messageText ) + let publicKey: String = { + switch threadVariant { + case .contact, .legacyGroup, .group: return threadId + case .community: return getUserHexEncodedPublicKey(using: dependencies) + } + }() shareNavController?.dismiss(animated: true, completion: nil) ModalActivityIndicatorViewController.present(fromViewController: shareNavController!, canCancel: false, message: "vc_share_sending_message".localized()) { activityIndicator in - Storage.resumeDatabaseAccess() + Storage.resumeDatabaseAccess(using: dependencies) /// When we prepare the message we set the timestamp to be the `SnodeAPI.currentOffsetTimestampMs()` /// but won't actually have a value because the share extension won't have talked to a service node yet which can cause @@ -246,18 +252,15 @@ final class ThreadPickerVC: UIViewController, UITableViewDataSource, UITableView .eraseToAnyPublisher() } .subscribe(on: DispatchQueue.global(qos: .userInitiated)) - .flatMap { _ in + .flatMap { _ -> AnyPublisher in SnodeAPI - .getSwarm( - for: { - switch threadVariant { - case .contact, .legacyGroup, .group: return threadId - case .community: return getUserHexEncodedPublicKey(using: dependencies) - } - }(), - using: dependencies - ) - .tryFlatMapWithRandomSnode { SnodeAPI.getNetworkTime(from: $0, using: dependencies) } + .getSwarm(for: publicKey, using: dependencies) + .tryFlatMapWithRandomSnode { snode in + Just(try SnodeAPI.preparedGetNetworkTime(from: snode, using: dependencies)) + .setFailureType(to: Error.self) + .eraseToAnyPublisher() + } + .map { $0.send(using: dependencies) } .map { _ in () } .eraseToAnyPublisher() } @@ -353,7 +356,7 @@ final class ThreadPickerVC: UIViewController, UITableViewDataSource, UITableView .receive(on: DispatchQueue.main) .sinkUntilComplete( receiveCompletion: { [weak self] result in - Storage.suspendDatabaseAccess() + Storage.suspendDatabaseAccess(using: dependencies) activityIndicator.dismiss { } switch result { diff --git a/SessionSnodeKit/Models/DeleteAllBeforeRequest.swift b/SessionSnodeKit/Models/DeleteAllBeforeRequest.swift index cdd1324db..7d7824c12 100644 --- a/SessionSnodeKit/Models/DeleteAllBeforeRequest.swift +++ b/SessionSnodeKit/Models/DeleteAllBeforeRequest.swift @@ -4,7 +4,7 @@ import Foundation import SessionUtilitiesKit extension SnodeAPI { - public class DeleteAllBeforeRequest: SnodeAuthenticatedRequestBody { + public final class DeleteAllBeforeRequest: SnodeAuthenticatedRequestBody, UpdatableTimestamp { enum CodingKeys: String, CodingKey { case beforeMs = "before" case namespace @@ -47,6 +47,17 @@ extension SnodeAPI { try super.encode(to: encoder) } + // MARK: - UpdatableTimestamp + + public func with(timestampMs: UInt64) -> DeleteAllBeforeRequest { + return DeleteAllBeforeRequest( + beforeMs: self.beforeMs, + namespace: self.namespace, + authInfo: self.authInfo, + timestampMs: timestampMs + ) + } + // MARK: - Abstract Methods override func generateSignature(using dependencies: Dependencies) throws -> [UInt8] { diff --git a/SessionSnodeKit/Models/DeleteAllMessagesRequest.swift b/SessionSnodeKit/Models/DeleteAllMessagesRequest.swift index 9acf2d67d..022d1223b 100644 --- a/SessionSnodeKit/Models/DeleteAllMessagesRequest.swift +++ b/SessionSnodeKit/Models/DeleteAllMessagesRequest.swift @@ -4,7 +4,7 @@ import Foundation import SessionUtilitiesKit extension SnodeAPI { - public class DeleteAllMessagesRequest: SnodeAuthenticatedRequestBody { + public final class DeleteAllMessagesRequest: SnodeAuthenticatedRequestBody, UpdatableTimestamp { enum CodingKeys: String, CodingKey { case namespace } @@ -45,6 +45,16 @@ extension SnodeAPI { try super.encode(to: encoder) } + // MARK: - UpdatableTimestamp + + public func with(timestampMs: UInt64) -> DeleteAllMessagesRequest { + return DeleteAllMessagesRequest( + namespace: self.namespace, + authInfo: self.authInfo, + timestampMs: timestampMs + ) + } + // MARK: - Abstract Methods override func generateSignature(using dependencies: Dependencies) throws -> [UInt8] { diff --git a/SessionSnodeKit/Models/SnodeRequest.swift b/SessionSnodeKit/Models/SnodeRequest.swift index dd2417eea..8205eea54 100644 --- a/SessionSnodeKit/Models/SnodeRequest.swift +++ b/SessionSnodeKit/Models/SnodeRequest.swift @@ -37,3 +37,14 @@ public struct SnodeRequest: Encodable { extension SnodeRequest: BatchRequestChildRetrievable where T: BatchRequestChildRetrievable { public var requests: [HTTP.BatchRequest.Child] { body.requests } } + +// MARK: - UpdatableTimestamp + +extension SnodeRequest: UpdatableTimestamp where T: UpdatableTimestamp { + public func with(timestampMs: UInt64) -> SnodeRequest { + return SnodeRequest( + endpoint: self.endpoint, + body: self.body.with(timestampMs: timestampMs) + ) + } +} diff --git a/SessionSnodeKit/Networking/PreparedRequest+OnionRequest.swift b/SessionSnodeKit/Networking/PreparedRequest+OnionRequest.swift index b34569d34..ff6c137e6 100644 --- a/SessionSnodeKit/Networking/PreparedRequest+OnionRequest.swift +++ b/SessionSnodeKit/Networking/PreparedRequest+OnionRequest.swift @@ -36,9 +36,21 @@ public extension HTTP.PreparedRequest { ) ) - case let randomSnode as HTTP.RandomSnodeTarget: + case let snodeTarget as HTTP.SnodeTarget: guard let payload: Data = request.httpBody else { throw HTTPError.invalidPreparedRequest } + + return dependencies[singleton: .network] + .send( + .onionRequest( + payload, + to: snodeTarget.snode, + timeout: timeout + ) + ) + case let randomSnode as HTTP.RandomSnodeTarget: + guard let payload: Data = request.httpBody else { throw HTTPError.invalidPreparedRequest } + return SnodeAPI.getSwarm(for: randomSnode.publicKey, using: dependencies) .tryFlatMapWithRandomSnode(retry: SnodeAPI.maxRetryCount) { snode in dependencies[singleton: .network] @@ -51,6 +63,42 @@ public extension HTTP.PreparedRequest { ) } + case let randomSnode as HTTP.RandomSnodeLatestNetworkTimeTarget: + guard request.httpBody != nil else { throw HTTPError.invalidPreparedRequest } + + return SnodeAPI.getSwarm(for: randomSnode.publicKey, using: dependencies) + .tryFlatMapWithRandomSnode(retry: SnodeAPI.maxRetryCount) { snode in + try SnodeAPI + .preparedGetNetworkTime(from: snode, using: dependencies) + .send(using: dependencies) + .tryFlatMap { _, timestampMs in + guard + let updatedRequest: URLRequest = try? randomSnode + .urlRequestWithUpdatedTimestampMs(timestampMs, dependencies), + let payload: Data = updatedRequest.httpBody + else { throw HTTPError.invalidPreparedRequest } + + return dependencies[singleton: .network] + .send( + .onionRequest( + payload, + to: snode, + timeout: timeout + ) + ) + .map { info, response -> (ResponseInfoType, Data?) in + ( + SnodeAPI.LatestTimestampResponseInfo( + code: info.code, + headers: info.headers, + timestampMs: timestampMs + ), + response + ) + } + } + } + default: throw HTTPError.invalidPreparedRequest } } diff --git a/SessionSnodeKit/Networking/Request+SnodeAPI.swift b/SessionSnodeKit/Networking/Request+SnodeAPI.swift index bcbbe0f50..e3e66f40d 100644 --- a/SessionSnodeKit/Networking/Request+SnodeAPI.swift +++ b/SessionSnodeKit/Networking/Request+SnodeAPI.swift @@ -3,18 +3,66 @@ import Foundation import SessionUtilitiesKit +// MARK: - SnodeTarget + +internal extension HTTP { + struct SnodeTarget: RequestTarget, Equatable { + let snode: Snode + + var url: URL? { URL(string: "snode:\(snode.x25519PublicKey)") } + var urlPathAndParamsString: String { return "" } + } +} + // MARK: - RandomSnodeTarget internal extension HTTP { struct RandomSnodeTarget: RequestTarget, Equatable { let publicKey: String - let requiresLatestNetworkTime: Bool var url: URL? { URL(string: "snode:\(publicKey)") } var urlPathAndParamsString: String { return "" } } } +// MARK: - RandomSnodeLatestNetworkTimeTarget + +internal extension HTTP { + struct RandomSnodeLatestNetworkTimeTarget: RequestTarget, Equatable { + let publicKey: String + let urlRequestWithUpdatedTimestampMs: ((UInt64, Dependencies) throws -> URLRequest) + + var url: URL? { URL(string: "snode:\(publicKey)") } + var urlPathAndParamsString: String { return "" } + + static func == (lhs: HTTP.RandomSnodeLatestNetworkTimeTarget, rhs: HTTP.RandomSnodeLatestNetworkTimeTarget) -> Bool { + lhs.publicKey == rhs.publicKey + } + } +} + +// MARK: Request - SnodeTarget + +public extension Request { + init( + method: HTTPMethod = .get, + endpoint: Endpoint, + snode: Snode, + headers: [HTTPHeader: String] = [:], + body: T? = nil + ) { + self = Request( + method: method, + endpoint: endpoint, + target: HTTP.SnodeTarget( + snode: snode + ), + headers: headers, + body: body + ) + } +} + // MARK: Request - RandomSnodeTarget public extension Request { @@ -29,8 +77,39 @@ public extension Request { method: method, endpoint: endpoint, target: HTTP.RandomSnodeTarget( + publicKey: publicKey + ), + headers: headers, + body: body + ) + } +} + +// MARK: Request - RandomSnodeLatestNetworkTimeTarget + +public extension Request { + init( + method: HTTPMethod = .get, + endpoint: Endpoint, + publicKey: String, + headers: [HTTPHeader: String] = [:], + requiresLatestNetworkTime: Bool, + body: T? = nil + ) where T: UpdatableTimestamp { + self = Request( + method: method, + endpoint: endpoint, + target: HTTP.RandomSnodeLatestNetworkTimeTarget( publicKey: publicKey, - requiresLatestNetworkTime: false // TODO: Sort this out + urlRequestWithUpdatedTimestampMs: { timestampMs, dependencies in + try Request( + method: method, + endpoint: endpoint, + publicKey: publicKey, + headers: headers, + body: body?.with(timestampMs: timestampMs) + ).generateUrlRequest(using: dependencies) + } ), headers: headers, body: body diff --git a/SessionSnodeKit/Networking/ResponseInfo+SnodeAPI.swift b/SessionSnodeKit/Networking/ResponseInfo+SnodeAPI.swift new file mode 100644 index 000000000..91d81a1ab --- /dev/null +++ b/SessionSnodeKit/Networking/ResponseInfo+SnodeAPI.swift @@ -0,0 +1,18 @@ +// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved. + +import Foundation +import SessionUtilitiesKit + +public extension SnodeAPI { + struct LatestTimestampResponseInfo: ResponseInfoType { + public let code: Int + public let headers: [String: String] + public let timestampMs: UInt64 + + public init(code: Int, headers: [String: String], timestampMs: UInt64) { + self.code = code + self.headers = headers + self.timestampMs = timestampMs + } + } +} diff --git a/SessionSnodeKit/Networking/SnodeAPI.swift b/SessionSnodeKit/Networking/SnodeAPI.swift index c8f09a2b6..2190d163d 100644 --- a/SessionSnodeKit/Networking/SnodeAPI.swift +++ b/SessionSnodeKit/Networking/SnodeAPI.swift @@ -861,111 +861,73 @@ public final class SnodeAPI { } /// Clears all the user's data from their swarm. Returns a dictionary of snode public key to deletion confirmation. - public static func deleteAllMessages( + public static func preparedDeleteAllMessages( namespace: SnodeAPI.Namespace, authInfo: AuthenticationInfo, using dependencies: Dependencies = Dependencies() - ) -> AnyPublisher<[String: Bool], Error> { - return getSwarm(for: authInfo.publicKey, using: dependencies) - .tryFlatMapWithRandomSnode(retry: maxRetryCount) { snode -> AnyPublisher<[String: Bool], Error> in - getNetworkTime(from: snode) - .flatMap { timestampMs -> AnyPublisher<[String: Bool], Error> in - SnodeAPI - .send( - request: SnodeRequest( - endpoint: .deleteAll, - body: DeleteAllMessagesRequest( - namespace: namespace, - authInfo: authInfo, - timestampMs: timestampMs - ) - ), - to: snode, - associatedWith: authInfo.publicKey, - using: dependencies - ) - .decoded(as: DeleteAllMessagesResponse.self, using: dependencies) - .tryMap { _, response -> [String: Bool] in - try response.validResultMap( - publicKey: authInfo.publicKey, - validationData: timestampMs, - using: dependencies - ) - } - .eraseToAnyPublisher() - } - .eraseToAnyPublisher() + ) throws -> HTTP.PreparedRequest<[String: Bool]> { + return try SnodeAPI + .prepareRequest( + request: Request( + endpoint: .deleteAll, + publicKey: authInfo.publicKey, + requiresLatestNetworkTime: true, + body: DeleteAllMessagesRequest( + namespace: namespace, + authInfo: authInfo, + timestampMs: UInt64(SnodeAPI.currentOffsetTimestampMs(using: dependencies)) + ) + ), + responseType: DeleteAllMessagesResponse.self, + retryCount: maxRetryCount + ) + .tryMap { info, response -> [String: Bool] in + guard let targetInfo: LatestTimestampResponseInfo = info as? LatestTimestampResponseInfo else { + throw HTTPError.invalidResponse + } + + return try response.validResultMap( + publicKey: authInfo.publicKey, + validationData: targetInfo.timestampMs, + using: dependencies + ) } } /// Clears all the user's data from their swarm. Returns a dictionary of snode public key to deletion confirmation. - public static func deleteAllMessages( + public static func preparedDeleteAllMessages( beforeMs: UInt64, namespace: SnodeAPI.Namespace, authInfo: AuthenticationInfo, using dependencies: Dependencies = Dependencies() - ) -> AnyPublisher<[String: Bool], Error> { - return getSwarm(for: authInfo.publicKey, using: dependencies) - .tryFlatMapWithRandomSnode(retry: maxRetryCount) { snode -> AnyPublisher<[String: Bool], Error> in - getNetworkTime(from: snode) - .flatMap { timestampMs -> AnyPublisher<[String: Bool], Error> in - SnodeAPI - .send( - request: SnodeRequest( - endpoint: .deleteAllBefore, - body: DeleteAllBeforeRequest( - beforeMs: beforeMs, - namespace: namespace, - authInfo: authInfo, - timestampMs: timestampMs - ) - ), - to: snode, - associatedWith: authInfo.publicKey, - using: dependencies - ) - .decoded(as: DeleteAllBeforeResponse.self, using: dependencies) - .tryMap { _, response -> [String: Bool] in - try response.validResultMap( - publicKey: authInfo.publicKey, - validationData: beforeMs, - using: dependencies - ) - } - .eraseToAnyPublisher() - } - .eraseToAnyPublisher() - } - } - - // MARK: - Internal API - - public static func getNetworkTime( - from snode: Snode, - using dependencies: Dependencies = Dependencies() - ) -> AnyPublisher { - return SnodeAPI - .send( - request: SnodeRequest<[String: String]>( - endpoint: .getInfo, - body: [:] + ) throws -> HTTP.PreparedRequest<[String: Bool]> { + return try SnodeAPI + .prepareRequest( + request: Request( + endpoint: .deleteAllBefore, + publicKey: authInfo.publicKey, + requiresLatestNetworkTime: true, + body: DeleteAllBeforeRequest( + beforeMs: beforeMs, + namespace: namespace, + authInfo: authInfo, + timestampMs: UInt64(SnodeAPI.currentOffsetTimestampMs(using: dependencies)) + ) ), - to: snode, - associatedWith: nil, - using: dependencies + responseType: DeleteAllMessagesResponse.self, + retryCount: maxRetryCount ) - .decoded(as: GetNetworkTimestampResponse.self, using: dependencies) - .map { _, response in - // Assume we've fetched the networkTime in order to send a message to the specified snode, in - // which case we want to update the 'clockOffsetMs' value for subsequent requests - let offset = (Int64(response.timestamp) - Int64(floor(dependencies.dateNow.timeIntervalSince1970 * 1000))) - SnodeAPI.clockOffsetMs.mutate { $0 = offset } - - return response.timestamp + .tryMap { _, response -> [String: Bool] in + try response.validResultMap( + publicKey: authInfo.publicKey, + validationData: beforeMs, + using: dependencies + ) } - .eraseToAnyPublisher() } + // MARK: - Internal API + public static func preparedGetNetworkTime( from snode: Snode, using dependencies: Dependencies = Dependencies() @@ -974,7 +936,7 @@ public final class SnodeAPI { .prepareRequest( request: Request, Endpoint>( endpoint: .getInfo, - publicKey: snode.x25519PublicKey, + snode: snode, body: [:] ), responseType: GetNetworkTimestampResponse.self @@ -1441,4 +1403,38 @@ private extension Request { ) ) } + + init( + endpoint: SnodeAPI.Endpoint, + snode: Snode, + body: B + ) where T == SnodeRequest, Endpoint == SnodeAPI.Endpoint { + self = Request( + method: .post, + endpoint: endpoint, + snode: snode, + body: SnodeRequest( + endpoint: endpoint, + body: body + ) + ) + } + + init( + endpoint: SnodeAPI.Endpoint, + publicKey: String, + requiresLatestNetworkTime: Bool, + body: B + ) where T == SnodeRequest, Endpoint == SnodeAPI.Endpoint, B: Encodable & UpdatableTimestamp { + self = Request( + method: .post, + endpoint: endpoint, + publicKey: publicKey, + requiresLatestNetworkTime: requiresLatestNetworkTime, + body: SnodeRequest( + endpoint: endpoint, + body: body + ) + ) + } } diff --git a/SessionSnodeKit/Types/UpdatableTimestamp.swift b/SessionSnodeKit/Types/UpdatableTimestamp.swift new file mode 100644 index 000000000..4847a9445 --- /dev/null +++ b/SessionSnodeKit/Types/UpdatableTimestamp.swift @@ -0,0 +1,7 @@ +// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +public protocol UpdatableTimestamp { + func with(timestampMs: UInt64) -> Self +} diff --git a/SessionUtilitiesKit/Networking/ResponseInfo.swift b/SessionUtilitiesKit/Networking/ResponseInfo.swift index df4b2d3d5..ddc3572e4 100644 --- a/SessionUtilitiesKit/Networking/ResponseInfo.swift +++ b/SessionUtilitiesKit/Networking/ResponseInfo.swift @@ -18,4 +18,3 @@ public extension HTTP { } } } -