mirror of https://github.com/oxen-io/session-ios
				
				
				
			
			You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
	
	
		
			1188 lines
		
	
	
		
			53 KiB
		
	
	
	
		
			Swift
		
	
			
		
		
	
	
			1188 lines
		
	
	
		
			53 KiB
		
	
	
	
		
			Swift
		
	
| // Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
 | |
| //
 | |
| // stringlint:disable
 | |
| 
 | |
| import Foundation
 | |
| import Combine
 | |
| import Sodium
 | |
| import GRDB
 | |
| import SessionUtilitiesKit
 | |
| 
 | |
| public extension Network.RequestType {
 | |
|     static func message(
 | |
|         _ message: SnodeMessage,
 | |
|         in namespace: SnodeAPI.Namespace
 | |
|     ) -> Network.RequestType<SendMessagesResponse> {
 | |
|         return Network.RequestType(id: "snodeAPI.sendMessage", args: [message, namespace]) {
 | |
|             SnodeAPI.sendMessage(message, in: namespace, using: $0)
 | |
|         }
 | |
|     }
 | |
| }
 | |
| 
 | |
| public final class SnodeAPI {
 | |
|     internal static let sodium: Atomic<Sodium> = Atomic(Sodium())
 | |
|     
 | |
|     /// The offset between the user's clock and the Service Node's clock. Used in cases where the
 | |
|     /// user's clock is incorrect.
 | |
|     ///
 | |
|     /// - Note: Should only be accessed from `Threading.workQueue` to avoid race conditions.
 | |
|     public static var clockOffsetMs: Atomic<Int64> = Atomic(0)
 | |
|     
 | |
|     // MARK: - Hardfork version
 | |
|     
 | |
|     public static var hardfork = UserDefaults.standard[.hardfork]
 | |
|     public static var softfork = UserDefaults.standard[.softfork]
 | |
| 
 | |
|     // MARK: - Settings
 | |
|     
 | |
|     public static let maxRetryCount: Int = 8
 | |
|     
 | |
|     public static func currentOffsetTimestampMs() -> Int64 {
 | |
|         return Int64(
 | |
|             Int64(floor(Date().timeIntervalSince1970 * 1000)) +
 | |
|             SnodeAPI.clockOffsetMs.wrappedValue
 | |
|         )
 | |
|     }
 | |
| 
 | |
|     // MARK: - Batching & Polling
 | |
|     
 | |
|     public static func poll(
 | |
|         namespaces: [SnodeAPI.Namespace],
 | |
|         refreshingConfigHashes: [String] = [],
 | |
|         from snode: LibSession.Snode,
 | |
|         swarmPublicKey: String,
 | |
|         using dependencies: Dependencies
 | |
|     ) -> AnyPublisher<[SnodeAPI.Namespace: (info: ResponseInfoType, data: (messages: [SnodeReceivedMessage], lastHash: String?)?)], Error> {
 | |
|         guard let userED25519KeyPair = Identity.fetchUserEd25519KeyPair() else {
 | |
|             return Fail(error: SnodeAPIError.noKeyPair)
 | |
|                 .eraseToAnyPublisher()
 | |
|         }
 | |
|         
 | |
|         let userX25519PublicKey: String = getUserHexEncodedPublicKey(using: dependencies)
 | |
|         
 | |
|         // Prune expired message hashes for this namespace on this service node
 | |
|         return Just(())
 | |
|             .setFailureType(to: Error.self)
 | |
|             .map { _ -> [SnodeAPI.Namespace: String] in
 | |
|                 namespaces.reduce(into: [:]) { result, namespace in
 | |
|                     guard namespace.shouldFetchSinceLastHash else { return }
 | |
|                     
 | |
|                     // Prune expired message hashes for this namespace on this service node
 | |
|                     SnodeReceivedMessageInfo.pruneExpiredMessageHashInfo(
 | |
|                         for: snode,
 | |
|                         namespace: namespace,
 | |
|                         associatedWith: swarmPublicKey,
 | |
|                         using: dependencies
 | |
|                     )
 | |
|                     
 | |
|                     result[namespace] = SnodeReceivedMessageInfo
 | |
|                         .fetchLastNotExpired(
 | |
|                             for: snode,
 | |
|                             namespace: namespace,
 | |
|                             associatedWith: swarmPublicKey,
 | |
|                             using: dependencies
 | |
|                         )?
 | |
|                         .hash
 | |
|                 }
 | |
|             }
 | |
|             .tryFlatMap { namespaceLastHash -> AnyPublisher<[SnodeAPI.Namespace: (info: ResponseInfoType, data: (messages: [SnodeReceivedMessage], lastHash: String?)?)], Error> in
 | |
|                 var requests: [any ErasedPreparedRequest] = []
 | |
|                 
 | |
|                 // If we have any config hashes to refresh TTLs then add those requests first
 | |
|                 if !refreshingConfigHashes.isEmpty {
 | |
|                     requests.append(
 | |
|                         try SnodeAPI.prepareRequest(
 | |
|                             request: Request(
 | |
|                                 endpoint: .expire,
 | |
|                                 swarmPublicKey: swarmPublicKey,
 | |
|                                 body: UpdateExpiryRequest(
 | |
|                                     messageHashes: refreshingConfigHashes,
 | |
|                                     expiryMs: UInt64(
 | |
|                                         SnodeAPI.currentOffsetTimestampMs() +
 | |
|                                         (30 * 24 * 60 * 60 * 1000) // 30 days
 | |
|                                     ),
 | |
|                                     extend: true,
 | |
|                                     pubkey: userX25519PublicKey,
 | |
|                                     ed25519PublicKey: userED25519KeyPair.publicKey,
 | |
|                                     ed25519SecretKey: userED25519KeyPair.secretKey,
 | |
|                                     subkey: nil    // TODO: Need to get this
 | |
|                                 )
 | |
|                             ),
 | |
|                             responseType: UpdateExpiryResponse.self,
 | |
|                             using: dependencies
 | |
|                         )
 | |
|                     )
 | |
|                 }
 | |
|                 
 | |
|                 // Determine the maxSize each namespace in the request should take up
 | |
|                 let namespaceMaxSizeMap: [SnodeAPI.Namespace: Int64] = SnodeAPI.Namespace.maxSizeMap(for: namespaces)
 | |
|                 let fallbackSize: Int64 = (namespaceMaxSizeMap.values.min() ?? 1)
 | |
|                 
 | |
|                 // Add the various 'getMessages' requests
 | |
|                 requests.append(
 | |
|                     contentsOf: try namespaces.map { namespace -> any ErasedPreparedRequest in
 | |
|                         // Check if this namespace requires authentication
 | |
|                         guard namespace.requiresReadAuthentication else {
 | |
|                             return try SnodeAPI.prepareRequest(
 | |
|                                 request: Request(
 | |
|                                     endpoint: .getMessages,
 | |
|                                     swarmPublicKey: swarmPublicKey,
 | |
|                                     body: LegacyGetMessagesRequest(
 | |
|                                         pubkey: swarmPublicKey,
 | |
|                                         lastHash: (namespaceLastHash[namespace] ?? ""),
 | |
|                                         namespace: namespace,
 | |
|                                         maxCount: nil,
 | |
|                                         maxSize: namespaceMaxSizeMap[namespace]
 | |
|                                             .defaulting(to: fallbackSize)
 | |
|                                     )
 | |
|                                 ),
 | |
|                                 responseType: GetMessagesResponse.self,
 | |
|                                 using: dependencies
 | |
|                             )
 | |
|                         }
 | |
|                         
 | |
|                         return try SnodeAPI.prepareRequest(
 | |
|                             request: Request(
 | |
|                                 endpoint: .getMessages,
 | |
|                                 swarmPublicKey: swarmPublicKey,
 | |
|                                 body: GetMessagesRequest(
 | |
|                                     lastHash: (namespaceLastHash[namespace] ?? ""),
 | |
|                                     namespace: namespace,
 | |
|                                     pubkey: swarmPublicKey,
 | |
|                                     subkey: nil,    // TODO: Need to get this
 | |
|                                     timestampMs: UInt64(SnodeAPI.currentOffsetTimestampMs()),
 | |
|                                     ed25519PublicKey: userED25519KeyPair.publicKey,
 | |
|                                     ed25519SecretKey: userED25519KeyPair.secretKey,
 | |
|                                     maxSize: namespaceMaxSizeMap[namespace]
 | |
|                                         .defaulting(to: fallbackSize)
 | |
|                                 )
 | |
|                             ),
 | |
|                             responseType: GetMessagesResponse.self,
 | |
|                             using: dependencies
 | |
|                         )
 | |
|                     }
 | |
|                 )
 | |
|                 
 | |
|                 // Actually send the request
 | |
|                 return try SnodeAPI
 | |
|                     .prepareRequest(
 | |
|                         request: Request(
 | |
|                             endpoint: .batch,
 | |
|                             snode: snode,
 | |
|                             swarmPublicKey: swarmPublicKey,
 | |
|                             body: Network.BatchRequest(requestsKey: .requests, requests: requests)
 | |
|                         ),
 | |
|                         responseType: Network.BatchResponse.self,
 | |
|                         requireAllBatchResponses: true,
 | |
|                         using: dependencies
 | |
|                     )
 | |
|                     .send(using: dependencies)
 | |
|                     .map { (_: ResponseInfoType, batchResponse: Network.BatchResponse) -> [SnodeAPI.Namespace: (info: ResponseInfoType, data: (messages: [SnodeReceivedMessage], lastHash: String?)?)] in
 | |
|                         let messageResponses: [Network.BatchSubResponse<GetMessagesResponse>] = batchResponse
 | |
|                             .compactMap { $0 as? Network.BatchSubResponse<GetMessagesResponse> }
 | |
|                         
 | |
|                         /// Since we have extended the TTL for a number of messages we need to make sure we update the local
 | |
|                         /// `SnodeReceivedMessageInfo.expirationDateMs` values so we don't end up deleting them
 | |
|                         /// incorrectly before they actually expire on the swarm
 | |
|                         if
 | |
|                             !refreshingConfigHashes.isEmpty,
 | |
|                             let refreshTTLSubReponse: Network.BatchSubResponse<UpdateExpiryResponse> = batchResponse
 | |
|                                 .first(where: { $0 is Network.BatchSubResponse<UpdateExpiryResponse> })
 | |
|                                 .asType(Network.BatchSubResponse<UpdateExpiryResponse>.self),
 | |
|                             let refreshTTLResponse: UpdateExpiryResponse = refreshTTLSubReponse.body,
 | |
|                             let validResults: [String: UpdateExpiryResponseResult] = try? refreshTTLResponse.validResultMap(
 | |
|                                 sodium: sodium.wrappedValue,
 | |
|                                 userX25519PublicKey: getUserHexEncodedPublicKey(),
 | |
|                                 validationData: refreshingConfigHashes
 | |
|                             ),
 | |
|                             let targetResult: UpdateExpiryResponseResult = validResults[snode.ed25519PubkeyHex],
 | |
|                             let groupedExpiryResult: [UInt64: [String]] = targetResult.changed
 | |
|                                 .updated(with: targetResult.unchanged)
 | |
|                                 .groupedByValue()
 | |
|                                 .nullIfEmpty()
 | |
|                         {
 | |
|                             dependencies.storage.writeAsync { db in
 | |
|                                 try groupedExpiryResult.forEach { updatedExpiry, hashes in
 | |
|                                     try SnodeReceivedMessageInfo
 | |
|                                         .filter(hashes.contains(SnodeReceivedMessageInfo.Columns.hash))
 | |
|                                         .updateAll(
 | |
|                                             db,
 | |
|                                             SnodeReceivedMessageInfo.Columns.expirationDateMs
 | |
|                                                 .set(to: updatedExpiry)
 | |
|                                         )
 | |
|                                 }
 | |
|                             }
 | |
|                         }
 | |
|                         
 | |
|                         return zip(namespaces, messageResponses)
 | |
|                             .reduce(into: [:]) { result, next in
 | |
|                                 guard let messageResponse: GetMessagesResponse = next.1.body else { return }
 | |
|                                 
 | |
|                                 let namespace: SnodeAPI.Namespace = next.0
 | |
|                                 
 | |
|                                 result[namespace] = (
 | |
|                                     info: next.1,
 | |
|                                     data: (
 | |
|                                         messages: messageResponse.messages
 | |
|                                             .compactMap { rawMessage -> SnodeReceivedMessage? in
 | |
|                                                 SnodeReceivedMessage(
 | |
|                                                     snode: snode,
 | |
|                                                     publicKey: swarmPublicKey,
 | |
|                                                     namespace: namespace,
 | |
|                                                     rawMessage: rawMessage
 | |
|                                                 )
 | |
|                                             },
 | |
|                                         lastHash: namespaceLastHash[namespace]
 | |
|                                     )
 | |
|                                 )
 | |
|                             }
 | |
|                     }
 | |
|                     .eraseToAnyPublisher()
 | |
|         }
 | |
|         .eraseToAnyPublisher()
 | |
|     }
 | |
|     
 | |
|     public static func preparedSequence(
 | |
|         requests: [any ErasedPreparedRequest],
 | |
|         requireAllBatchResponses: Bool,
 | |
|         swarmPublicKey: String,
 | |
|         using dependencies: Dependencies
 | |
|     ) throws -> Network.PreparedRequest<Network.BatchResponse> {
 | |
|         return try SnodeAPI
 | |
|             .prepareRequest(
 | |
|                 request: Request(
 | |
|                     endpoint: .sequence,
 | |
|                     swarmPublicKey: swarmPublicKey,
 | |
|                     body: Network.BatchRequest(requestsKey: .requests, requests: requests)
 | |
|                 ),
 | |
|                 responseType: Network.BatchResponse.self,
 | |
|                 requireAllBatchResponses: requireAllBatchResponses,
 | |
|                 using: dependencies
 | |
|             )
 | |
|     }
 | |
|     
 | |
|     /// **Note:** This is the direct request to retrieve messages so should be retrieved automatically from the `poll()` method, in order to call
 | |
|     /// this directly remove the `@available` line
 | |
|     @available(*, unavailable, message: "Avoid using this directly, use the pre-built `poll()` method instead")
 | |
|     public static func getMessages(
 | |
|         in namespace: SnodeAPI.Namespace,
 | |
|         from snode: LibSession.Snode,
 | |
|         swarmPublicKey: String,
 | |
|         using dependencies: Dependencies = Dependencies()
 | |
|     ) -> AnyPublisher<(info: ResponseInfoType, data: (messages: [SnodeReceivedMessage], lastHash: String?)?), Error> {
 | |
|         return Deferred {
 | |
|             Future<String?, Error> { resolver in
 | |
|                 // Prune expired message hashes for this namespace on this service node
 | |
|                 SnodeReceivedMessageInfo.pruneExpiredMessageHashInfo(
 | |
|                     for: snode,
 | |
|                     namespace: namespace,
 | |
|                     associatedWith: swarmPublicKey,
 | |
|                     using: dependencies
 | |
|                 )
 | |
|                 
 | |
|                 let maybeLastHash: String? = SnodeReceivedMessageInfo
 | |
|                     .fetchLastNotExpired(
 | |
|                         for: snode,
 | |
|                         namespace: namespace,
 | |
|                         associatedWith: swarmPublicKey,
 | |
|                         using: dependencies
 | |
|                     )?
 | |
|                     .hash
 | |
|                 
 | |
|                 resolver(Result.success(maybeLastHash))
 | |
|             }
 | |
|         }
 | |
|         .tryFlatMap { lastHash -> AnyPublisher<(info: ResponseInfoType, data: GetMessagesResponse?, lastHash: String?), Error> in
 | |
|             guard namespace.requiresReadAuthentication else {
 | |
|                 return try SnodeAPI
 | |
|                     .prepareRequest(
 | |
|                         request: Request(
 | |
|                             endpoint: .getMessages,
 | |
|                             snode: snode,
 | |
|                             swarmPublicKey: swarmPublicKey,
 | |
|                             body: LegacyGetMessagesRequest(
 | |
|                                 pubkey: swarmPublicKey,
 | |
|                                 lastHash: (lastHash ?? ""),
 | |
|                                 namespace: namespace,
 | |
|                                 maxCount: nil,
 | |
|                                 maxSize: nil
 | |
|                             )
 | |
|                         ),
 | |
|                         responseType: GetMessagesResponse.self,
 | |
|                         using: dependencies
 | |
|                     )
 | |
|                     .send(using: dependencies)
 | |
|                     .map { info, data in (info, data, lastHash) }
 | |
|                     .eraseToAnyPublisher()
 | |
|             }
 | |
|             
 | |
|             guard let userED25519KeyPair: KeyPair = Storage.shared.read({ db in Identity.fetchUserEd25519KeyPair(db) }) else {
 | |
|                 throw SnodeAPIError.noKeyPair
 | |
|             }
 | |
|             
 | |
|             return try SnodeAPI
 | |
|                 .prepareRequest(
 | |
|                     request: Request(
 | |
|                         endpoint: .getMessages,
 | |
|                         snode: snode,
 | |
|                         swarmPublicKey: swarmPublicKey,
 | |
|                         body: GetMessagesRequest(
 | |
|                             lastHash: (lastHash ?? ""),
 | |
|                             namespace: namespace,
 | |
|                             pubkey: swarmPublicKey,
 | |
|                             subkey: nil,
 | |
|                             timestampMs: UInt64(SnodeAPI.currentOffsetTimestampMs()),
 | |
|                             ed25519PublicKey: userED25519KeyPair.publicKey,
 | |
|                             ed25519SecretKey: userED25519KeyPair.secretKey
 | |
|                         )
 | |
|                     ),
 | |
|                     responseType: GetMessagesResponse.self,
 | |
|                     using: dependencies
 | |
|                 )
 | |
|                 .send(using: dependencies)
 | |
|                 .map { info, data in (info, data, lastHash) }
 | |
|                 .eraseToAnyPublisher()
 | |
|         }
 | |
|         .map { info, data, lastHash -> (info: ResponseInfoType, data: (messages: [SnodeReceivedMessage], lastHash: String?)?) in
 | |
|             return (
 | |
|                 info: info,
 | |
|                 data: data.map { messageResponse -> (messages: [SnodeReceivedMessage], lastHash: String?) in
 | |
|                     return (
 | |
|                         messages: messageResponse.messages
 | |
|                             .compactMap { rawMessage -> SnodeReceivedMessage? in
 | |
|                                 SnodeReceivedMessage(
 | |
|                                     snode: snode,
 | |
|                                     publicKey: swarmPublicKey,
 | |
|                                     namespace: namespace,
 | |
|                                     rawMessage: rawMessage
 | |
|                                 )
 | |
|                             },
 | |
|                         lastHash: lastHash
 | |
|                     )
 | |
|                 }
 | |
|             )
 | |
|         }
 | |
|         .eraseToAnyPublisher()
 | |
|     }
 | |
|     
 | |
|     public static func getSessionID(
 | |
|         for onsName: String,
 | |
|         using dependencies: Dependencies = Dependencies()
 | |
|     ) -> AnyPublisher<String, Error> {
 | |
|         let validationCount = 3
 | |
|         
 | |
|         // The name must be lowercased
 | |
|         let onsName = onsName.lowercased()
 | |
|         
 | |
|         // Hash the ONS name using BLAKE2b
 | |
|         let nameAsData = [UInt8](onsName.data(using: String.Encoding.utf8)!)
 | |
|         
 | |
|         guard let nameHash = sodium.wrappedValue.genericHash.hash(message: nameAsData) else {
 | |
|             return Fail(error: SnodeAPIError.onsHashingFailed)
 | |
|                 .eraseToAnyPublisher()
 | |
|         }
 | |
|         
 | |
|         // Ask 3 different snodes for the Session ID associated with the given name hash
 | |
|         let base64EncodedNameHash = nameHash.toBase64()
 | |
|         
 | |
|         return LibSession
 | |
|             .getRandomNodes(count: validationCount)
 | |
|             .tryFlatMap { nodes in
 | |
|                 Publishers.MergeMany(
 | |
|                     try nodes.map { snode in
 | |
|                         try SnodeAPI
 | |
|                             .prepareRequest(
 | |
|                                 request: Request(
 | |
|                                     endpoint: .oxenDaemonRPCCall,
 | |
|                                     snode: snode,
 | |
|                                     body: OxenDaemonRPCRequest(
 | |
|                                         endpoint: .daemonOnsResolve,
 | |
|                                         body: ONSResolveRequest(
 | |
|                                             type: 0, // type 0 means Session
 | |
|                                             base64EncodedNameHash: base64EncodedNameHash
 | |
|                                         )
 | |
|                                     )
 | |
|                                 ),
 | |
|                                 responseType: ONSResolveResponse.self,
 | |
|                                 using: dependencies
 | |
|                             )
 | |
|                             .tryMap { _, response -> String in
 | |
|                                 try response.sessionId(
 | |
|                                     sodium: sodium.wrappedValue,
 | |
|                                     nameBytes: nameAsData,
 | |
|                                     nameHashBytes: nameHash
 | |
|                                 )
 | |
|                             }
 | |
|                             .send(using: dependencies)
 | |
|                             .retry(4)
 | |
|                             .map { _, sessionId in sessionId }
 | |
|                             .eraseToAnyPublisher()
 | |
|                     }
 | |
|                 )
 | |
|             }
 | |
|             .collect()
 | |
|             .tryMap { results -> String in
 | |
|                 guard results.count == validationCount, Set(results).count == 1 else {
 | |
|                     throw SnodeAPIError.onsValidationFailed
 | |
|                 }
 | |
|                 
 | |
|                 return results[0]
 | |
|             }
 | |
|             .eraseToAnyPublisher()
 | |
|     }
 | |
|     
 | |
|     public static func getExpiries(
 | |
|         swarmPublicKey: String,
 | |
|         of serverHashes: [String],
 | |
|         using dependencies: Dependencies = Dependencies()
 | |
|     ) -> AnyPublisher<(ResponseInfoType, GetExpiriesResponse), Error> {
 | |
|         guard let userED25519KeyPair = Identity.fetchUserEd25519KeyPair() else {
 | |
|             return Fail(error: SnodeAPIError.noKeyPair)
 | |
|                 .eraseToAnyPublisher()
 | |
|         }
 | |
|         
 | |
|         let sendTimestamp: UInt64 = UInt64(SnodeAPI.currentOffsetTimestampMs())
 | |
|         
 | |
|         // FIXME: There is a bug on SS now that a single-hash lookup is not working. Remove it when the bug is fixed
 | |
|         let serverHashes: [String] = serverHashes.appending("///////////////////////////////////////////") // Fake hash with valid length
 | |
|         
 | |
|         do {
 | |
|             return try SnodeAPI
 | |
|                 .prepareRequest(
 | |
|                     request: Request(
 | |
|                         endpoint: .getExpiries,
 | |
|                         swarmPublicKey: swarmPublicKey,
 | |
|                         body: GetExpiriesRequest(
 | |
|                             messageHashes: serverHashes,
 | |
|                             pubkey: swarmPublicKey,
 | |
|                             subkey: nil,
 | |
|                             timestampMs: sendTimestamp,
 | |
|                             ed25519PublicKey: userED25519KeyPair.publicKey,
 | |
|                             ed25519SecretKey: userED25519KeyPair.secretKey
 | |
|                         )
 | |
|                     ),
 | |
|                     responseType: GetExpiriesResponse.self,
 | |
|                     using: dependencies
 | |
|                 )
 | |
|                 .send(using: dependencies)
 | |
|         }
 | |
|         catch { return Fail(error: error).eraseToAnyPublisher() }
 | |
|     }
 | |
|     
 | |
|     // MARK: - Store
 | |
|     
 | |
|     public static func preparedSendMessage(
 | |
|         _ db: Database,
 | |
|         message: SnodeMessage,
 | |
|         in namespace: Namespace,
 | |
|         using dependencies: Dependencies
 | |
|     ) throws -> Network.PreparedRequest<SendMessagesResponse> {
 | |
|         let swarmPublicKey: String = message.recipient
 | |
|         let userX25519PublicKey: String = getUserHexEncodedPublicKey()
 | |
|         
 | |
|         let request: Network.PreparedRequest<SendMessagesResponse> = try {
 | |
|             // Check if this namespace requires authentication
 | |
|             guard namespace.requiresWriteAuthentication else {
 | |
|                 return try SnodeAPI.prepareRequest(
 | |
|                     request: Request(
 | |
|                         endpoint: .sendMessage,
 | |
|                         swarmPublicKey: swarmPublicKey,
 | |
|                         body: LegacySendMessagesRequest(
 | |
|                             message: message,
 | |
|                             namespace: namespace
 | |
|                         )
 | |
|                     ),
 | |
|                     responseType: SendMessagesResponse.self,
 | |
|                     using: dependencies
 | |
|                 )
 | |
|             }
 | |
|             
 | |
|             guard let userED25519KeyPair: KeyPair = Identity.fetchUserEd25519KeyPair(db) else {
 | |
|                 throw SnodeAPIError.noKeyPair
 | |
|             }
 | |
|             
 | |
|             return try SnodeAPI.prepareRequest(
 | |
|                 request: Request(
 | |
|                     endpoint: .sendMessage,
 | |
|                     swarmPublicKey: swarmPublicKey,
 | |
|                     body: SendMessageRequest(
 | |
|                         message: message,
 | |
|                         namespace: namespace,
 | |
|                         subkey: nil,
 | |
|                         timestampMs: UInt64(SnodeAPI.currentOffsetTimestampMs()),
 | |
|                         ed25519PublicKey: userED25519KeyPair.publicKey,
 | |
|                         ed25519SecretKey: userED25519KeyPair.secretKey
 | |
|                     )
 | |
|                 ),
 | |
|                 responseType: SendMessagesResponse.self,
 | |
|                 using: dependencies
 | |
|             )
 | |
|         }()
 | |
|         
 | |
|         return request
 | |
|             .tryMap { _, response -> SendMessagesResponse in
 | |
|                 try response.validateResultMap(
 | |
|                     sodium: sodium.wrappedValue,
 | |
|                     userX25519PublicKey: userX25519PublicKey
 | |
|                 )
 | |
|                 
 | |
|                 return response
 | |
|             }
 | |
|     }
 | |
|     
 | |
|     public static func sendMessage(
 | |
|         _ message: SnodeMessage,
 | |
|         in namespace: Namespace,
 | |
|         using dependencies: Dependencies
 | |
|     ) -> AnyPublisher<(ResponseInfoType, SendMessagesResponse), Error> {
 | |
|         let swarmPublicKey: String = message.recipient
 | |
|         let userX25519PublicKey: String = getUserHexEncodedPublicKey()
 | |
|         
 | |
|         do {
 | |
|             let request: Network.PreparedRequest<SendMessagesResponse> = try {
 | |
|                 // Check if this namespace requires authentication
 | |
|                 guard namespace.requiresWriteAuthentication else {
 | |
|                     return try SnodeAPI.prepareRequest(
 | |
|                         request: Request(
 | |
|                             endpoint: .sendMessage,
 | |
|                             swarmPublicKey: swarmPublicKey,
 | |
|                             body: LegacySendMessagesRequest(
 | |
|                                 message: message,
 | |
|                                 namespace: namespace
 | |
|                             )
 | |
|                         ),
 | |
|                         responseType: SendMessagesResponse.self,
 | |
|                         using: dependencies
 | |
|                     )
 | |
|                 }
 | |
|                 
 | |
|                 guard let userED25519KeyPair: KeyPair = Storage.shared.read({ db in Identity.fetchUserEd25519KeyPair(db) }) else {
 | |
|                     throw SnodeAPIError.noKeyPair
 | |
|                 }
 | |
|                 
 | |
|                 return try SnodeAPI.prepareRequest(
 | |
|                     request: Request(
 | |
|                         endpoint: .sendMessage,
 | |
|                         swarmPublicKey: swarmPublicKey,
 | |
|                         body: SendMessageRequest(
 | |
|                             message: message,
 | |
|                             namespace: namespace,
 | |
|                             subkey: nil,
 | |
|                             timestampMs: UInt64(SnodeAPI.currentOffsetTimestampMs()),
 | |
|                             ed25519PublicKey: userED25519KeyPair.publicKey,
 | |
|                             ed25519SecretKey: userED25519KeyPair.secretKey
 | |
|                         )
 | |
|                     ),
 | |
|                     responseType: SendMessagesResponse.self,
 | |
|                     using: dependencies
 | |
|                 )
 | |
|             }()
 | |
|             
 | |
|             return request
 | |
|                 .tryMap { info, response -> SendMessagesResponse in
 | |
|                     try response.validateResultMap(
 | |
|                         sodium: sodium.wrappedValue,
 | |
|                         userX25519PublicKey: userX25519PublicKey
 | |
|                     )
 | |
|                     
 | |
|                     return response
 | |
|                 }
 | |
|                 .send(using: dependencies)
 | |
|         }
 | |
|         catch { return Fail(error: error).eraseToAnyPublisher() }
 | |
|     }
 | |
|     
 | |
|     public static func sendConfigMessages(
 | |
|         _ messages: [(message: SnodeMessage, namespace: Namespace)],
 | |
|         allObsoleteHashes: [String],
 | |
|         swarmPublicKey: String,
 | |
|         using dependencies: Dependencies = Dependencies()
 | |
|     ) -> AnyPublisher<Network.BatchResponse, Error> {
 | |
|         guard !messages.isEmpty || !allObsoleteHashes.isEmpty else {
 | |
|             return Fail(error: NetworkError.invalidPreparedRequest)
 | |
|                 .eraseToAnyPublisher()
 | |
|         }
 | |
|         // TODO: Need to get either the closed group subKey or the userEd25519 key for auth
 | |
|         guard let userED25519KeyPair = Identity.fetchUserEd25519KeyPair() else {
 | |
|             return Fail(error: SnodeAPIError.noKeyPair)
 | |
|                 .eraseToAnyPublisher()
 | |
|         }
 | |
|         
 | |
|         do {
 | |
|             let userX25519PublicKey: String = getUserHexEncodedPublicKey()
 | |
|             var requests: [any ErasedPreparedRequest] = try messages
 | |
|                 .map { message, namespace in
 | |
|                     // Check if this namespace requires authentication
 | |
|                     guard namespace.requiresWriteAuthentication else {
 | |
|                         return try SnodeAPI.prepareRequest(
 | |
|                             request: Request(
 | |
|                                 endpoint: .sendMessage,
 | |
|                                 swarmPublicKey: swarmPublicKey,
 | |
|                                 body: LegacySendMessagesRequest(
 | |
|                                     message: message,
 | |
|                                     namespace: namespace
 | |
|                                 )
 | |
|                             ),
 | |
|                             responseType: SendMessagesResponse.self,
 | |
|                             using: dependencies
 | |
|                         )
 | |
|                     }
 | |
|                     
 | |
|                     return try SnodeAPI.prepareRequest(
 | |
|                         request: Request(
 | |
|                             endpoint: .sendMessage,
 | |
|                             swarmPublicKey: swarmPublicKey,
 | |
|                             body: SendMessageRequest(
 | |
|                                 message: message,
 | |
|                                 namespace: namespace,
 | |
|                                 subkey: nil,    // TODO: Need to get this
 | |
|                                 timestampMs: UInt64(SnodeAPI.currentOffsetTimestampMs()),
 | |
|                                 ed25519PublicKey: userED25519KeyPair.publicKey,
 | |
|                                 ed25519SecretKey: userED25519KeyPair.secretKey
 | |
|                             )
 | |
|                         ),
 | |
|                         responseType: SendMessagesResponse.self,
 | |
|                         using: dependencies
 | |
|                     )
 | |
|                 }
 | |
|             
 | |
|             // If we had any previous config messages then we should delete them
 | |
|             if !allObsoleteHashes.isEmpty {
 | |
|                 requests.append(
 | |
|                     try SnodeAPI.prepareRequest(
 | |
|                         request: Request(
 | |
|                             endpoint: .deleteMessages,
 | |
|                             swarmPublicKey: swarmPublicKey,
 | |
|                             body: DeleteMessagesRequest(
 | |
|                                 messageHashes: allObsoleteHashes,
 | |
|                                 requireSuccessfulDeletion: false,
 | |
|                                 swarmPublicKey: userX25519PublicKey,
 | |
|                                 ed25519PublicKey: userED25519KeyPair.publicKey,
 | |
|                                 ed25519SecretKey: userED25519KeyPair.secretKey
 | |
|                             )
 | |
|                         ),
 | |
|                         responseType: DeleteMessagesResponse.self,
 | |
|                         using: dependencies
 | |
|                     )
 | |
|                 )
 | |
|             }
 | |
|             
 | |
|             return try SnodeAPI
 | |
|                 .prepareRequest(
 | |
|                     request: Request(
 | |
|                         endpoint: .sequence,
 | |
|                         swarmPublicKey: swarmPublicKey,
 | |
|                         body: Network.BatchRequest(requestsKey: .requests, requests: requests)
 | |
|                     ),
 | |
|                     responseType: Network.BatchResponse.self,
 | |
|                     requireAllBatchResponses: false,
 | |
|                     using: dependencies
 | |
|                 )
 | |
|                 .send(using: dependencies)
 | |
|                 .map { _, response in response }
 | |
|                 .eraseToAnyPublisher()
 | |
|         }
 | |
|         catch { return Fail(error: error).eraseToAnyPublisher() }
 | |
|     }
 | |
|     
 | |
|     // MARK: - Edit
 | |
|     
 | |
|     public static func updateExpiry(
 | |
|         swarmPublicKey: String,
 | |
|         serverHashes: [String],
 | |
|         updatedExpiryMs: Int64,
 | |
|         shortenOnly: Bool? = nil,
 | |
|         extendOnly: Bool? = nil,
 | |
|         using dependencies: Dependencies = Dependencies()
 | |
|     ) -> AnyPublisher<[String: UpdateExpiryResponseResult], Error> {
 | |
|         guard let userED25519KeyPair = Identity.fetchUserEd25519KeyPair() else {
 | |
|             return Fail(error: SnodeAPIError.noKeyPair)
 | |
|                 .eraseToAnyPublisher()
 | |
|         }
 | |
|         
 | |
|         // ShortenOnly and extendOnly cannot be true at the same time
 | |
|         guard shortenOnly == nil || extendOnly == nil else {
 | |
|             return Fail(error: NetworkError.invalidPreparedRequest)
 | |
|                 .eraseToAnyPublisher()
 | |
|         }
 | |
|         
 | |
|         // FIXME: There is a bug on SS now that a single-hash lookup is not working. Remove it when the bug is fixed
 | |
|         let serverHashes: [String] = serverHashes.appending("///////////////////////////////////////////") // Fake hash with valid length
 | |
|         
 | |
|         do {
 | |
|             return try SnodeAPI
 | |
|                 .prepareRequest(
 | |
|                     request: Request(
 | |
|                         endpoint: .expire,
 | |
|                         swarmPublicKey: swarmPublicKey,
 | |
|                         body: UpdateExpiryRequest(
 | |
|                             messageHashes: serverHashes,
 | |
|                             expiryMs: UInt64(updatedExpiryMs),
 | |
|                             shorten: shortenOnly,
 | |
|                             extend: extendOnly,
 | |
|                             pubkey: swarmPublicKey,
 | |
|                             ed25519PublicKey: userED25519KeyPair.publicKey,
 | |
|                             ed25519SecretKey: userED25519KeyPair.secretKey,
 | |
|                             subkey: nil
 | |
|                         )
 | |
|                     ),
 | |
|                     responseType: UpdateExpiryResponse.self,
 | |
|                     using: dependencies
 | |
|                 )
 | |
|                 .send(using: dependencies)
 | |
|                 .tryMap { _, response -> [String: UpdateExpiryResponseResult] in
 | |
|                     try response.validResultMap(
 | |
|                         sodium: sodium.wrappedValue,
 | |
|                         userX25519PublicKey: getUserHexEncodedPublicKey(),
 | |
|                         validationData: serverHashes
 | |
|                     )
 | |
|                 }
 | |
|                 .eraseToAnyPublisher()
 | |
|         }
 | |
|         catch { return Fail(error: error).eraseToAnyPublisher() }
 | |
|     }
 | |
|     
 | |
|     public static func revokeSubkey(
 | |
|         swarmPublicKey: String,
 | |
|         subkeyToRevoke: String,
 | |
|         using dependencies: Dependencies = Dependencies()
 | |
|     ) -> AnyPublisher<Void, Error> {
 | |
|         guard let userED25519KeyPair = Identity.fetchUserEd25519KeyPair() else {
 | |
|             return Fail(error: SnodeAPIError.noKeyPair)
 | |
|                 .eraseToAnyPublisher()
 | |
|         }
 | |
|         
 | |
|         do {
 | |
|             return try SnodeAPI
 | |
|                 .prepareRequest(
 | |
|                     request: Request(
 | |
|                         endpoint: .revokeSubaccount,
 | |
|                         swarmPublicKey: swarmPublicKey,
 | |
|                         body: RevokeSubkeyRequest(
 | |
|                             subkeyToRevoke: subkeyToRevoke,
 | |
|                             pubkey: swarmPublicKey,
 | |
|                             ed25519PublicKey: userED25519KeyPair.publicKey,
 | |
|                             ed25519SecretKey: userED25519KeyPair.secretKey
 | |
|                         )
 | |
|                     ),
 | |
|                     responseType: RevokeSubkeyResponse.self,
 | |
|                     using: dependencies
 | |
|                 )
 | |
|                 .send(using: dependencies)
 | |
|                 .tryMap { _, response -> Void in
 | |
|                     try response.validateResultMap(
 | |
|                         sodium: sodium.wrappedValue,
 | |
|                         userX25519PublicKey: getUserHexEncodedPublicKey(),
 | |
|                         validationData: subkeyToRevoke
 | |
|                     )
 | |
|                     
 | |
|                     return ()
 | |
|                 }
 | |
|                 .eraseToAnyPublisher()
 | |
|         }
 | |
|         catch { return Fail(error: error).eraseToAnyPublisher() }
 | |
|     }
 | |
|     
 | |
|     // MARK: Delete
 | |
|     
 | |
|     public static func preparedDeleteMessages(
 | |
|         _ db: Database,
 | |
|         swarmPublicKey: String,
 | |
|         serverHashes: [String],
 | |
|         requireSuccessfulDeletion: Bool,
 | |
|         using dependencies: Dependencies
 | |
|     ) throws -> Network.PreparedRequest<[String: Bool]> {
 | |
|         guard let userED25519KeyPair = Identity.fetchUserEd25519KeyPair(db) else { throw SnodeAPIError.noKeyPair }
 | |
|         
 | |
|         return try SnodeAPI
 | |
|             .prepareRequest(
 | |
|                 request: Request(
 | |
|                     endpoint: .deleteMessages,
 | |
|                     swarmPublicKey: swarmPublicKey,
 | |
|                     body: DeleteMessagesRequest(
 | |
|                         messageHashes: serverHashes,
 | |
|                         requireSuccessfulDeletion: requireSuccessfulDeletion,
 | |
|                         swarmPublicKey: swarmPublicKey,
 | |
|                         ed25519PublicKey: userED25519KeyPair.publicKey,
 | |
|                         ed25519SecretKey: userED25519KeyPair.secretKey
 | |
|                     )
 | |
|                 ),
 | |
|                 responseType: DeleteMessagesResponse.self,
 | |
|                 using: dependencies
 | |
|             )
 | |
|             .tryMap { _, response -> [String: Bool] in
 | |
|                 let validResultMap: [String: Bool] = try response.validResultMap(
 | |
|                     sodium: sodium.wrappedValue,
 | |
|                     userX25519PublicKey: swarmPublicKey,
 | |
|                     validationData: serverHashes
 | |
|                 )
 | |
|                 
 | |
|                 // If `validResultMap` didn't throw then at least one service node
 | |
|                 // deleted successfully so we should mark the hash as invalid so we
 | |
|                 // don't try to fetch updates using that hash going forward (if we
 | |
|                 // do we would end up re-fetching all old messages)
 | |
|                 Storage.shared.writeAsync { db in
 | |
|                     try? SnodeReceivedMessageInfo.handlePotentialDeletedOrInvalidHash(
 | |
|                         db,
 | |
|                         potentiallyInvalidHashes: serverHashes
 | |
|                     )
 | |
|                 }
 | |
|                 
 | |
|                 return validResultMap
 | |
|             }
 | |
|     }
 | |
|     
 | |
|     public static func deleteMessages(
 | |
|         swarmPublicKey: String,
 | |
|         serverHashes: [String],
 | |
|         using dependencies: Dependencies = Dependencies()
 | |
|     ) -> AnyPublisher<[String: Bool], Error> {
 | |
|         guard let userED25519KeyPair = Identity.fetchUserEd25519KeyPair() else {
 | |
|             return Fail(error: SnodeAPIError.noKeyPair)
 | |
|                 .eraseToAnyPublisher()
 | |
|         }
 | |
|         
 | |
|         let userX25519PublicKey: String = getUserHexEncodedPublicKey(using: dependencies)
 | |
|         
 | |
|         do {
 | |
|             return try SnodeAPI
 | |
|                 .prepareRequest(
 | |
|                     request: Request(
 | |
|                         endpoint: .deleteMessages,
 | |
|                         swarmPublicKey: swarmPublicKey,
 | |
|                         body: DeleteMessagesRequest(
 | |
|                             messageHashes: serverHashes,
 | |
|                             requireSuccessfulDeletion: false,
 | |
|                             swarmPublicKey: userX25519PublicKey,
 | |
|                             ed25519PublicKey: userED25519KeyPair.publicKey,
 | |
|                             ed25519SecretKey: userED25519KeyPair.secretKey
 | |
|                         )
 | |
|                     ),
 | |
|                     responseType: DeleteMessagesResponse.self,
 | |
|                     using: dependencies
 | |
|                 )
 | |
|                 .send(using: dependencies)
 | |
|                 .tryMap { _, response -> [String: Bool] in
 | |
|                     let validResultMap: [String: Bool] = try response.validResultMap(
 | |
|                         sodium: sodium.wrappedValue,
 | |
|                         userX25519PublicKey: userX25519PublicKey,
 | |
|                         validationData: serverHashes
 | |
|                     )
 | |
|                     
 | |
|                     // If `validResultMap` didn't throw then at least one service node
 | |
|                     // deleted successfully so we should mark the hash as invalid so we
 | |
|                     // don't try to fetch updates using that hash going forward (if we
 | |
|                     // do we would end up re-fetching all old messages)
 | |
|                     Storage.shared.writeAsync { db in
 | |
|                         try? SnodeReceivedMessageInfo.handlePotentialDeletedOrInvalidHash(
 | |
|                             db,
 | |
|                             potentiallyInvalidHashes: serverHashes
 | |
|                         )
 | |
|                     }
 | |
|                     
 | |
|                     return validResultMap
 | |
|                 }
 | |
|                 .eraseToAnyPublisher()
 | |
|         }
 | |
|         catch { return Fail(error: error).eraseToAnyPublisher() }
 | |
|     }
 | |
|     
 | |
|     /// Clears all the user's data from their swarm. Returns a dictionary of snode public key to deletion confirmation.
 | |
|     public static func deleteAllMessages(
 | |
|         namespace: SnodeAPI.Namespace,
 | |
|         using dependencies: Dependencies = Dependencies()
 | |
|     ) -> AnyPublisher<[String: Bool], Error> {
 | |
|         guard let userED25519KeyPair = Identity.fetchUserEd25519KeyPair() else {
 | |
|             return Fail(error: SnodeAPIError.noKeyPair)
 | |
|                 .eraseToAnyPublisher()
 | |
|         }
 | |
|         
 | |
|         let userX25519PublicKey: String = getUserHexEncodedPublicKey()
 | |
|         
 | |
|         do {
 | |
|             return try SnodeAPI
 | |
|                 .prepareRequest(
 | |
|                     request: Request(
 | |
|                         endpoint: .deleteAll,
 | |
|                         swarmPublicKey: userX25519PublicKey,
 | |
|                         requiresLatestNetworkTime: true,
 | |
|                         body: DeleteAllMessagesRequest(
 | |
|                             namespace: namespace,
 | |
|                             pubkey: userX25519PublicKey,
 | |
|                             timestampMs: UInt64(SnodeAPI.currentOffsetTimestampMs()),
 | |
|                             ed25519PublicKey: userED25519KeyPair.publicKey,
 | |
|                             ed25519SecretKey: userED25519KeyPair.secretKey
 | |
|                         )
 | |
|                     ),
 | |
|                     responseType: DeleteAllMessagesResponse.self,
 | |
|                     using: dependencies
 | |
|                 )
 | |
|                 .send(using: dependencies)
 | |
|                 .tryMap { info, response -> [String: Bool] in
 | |
|                     guard let targetInfo: LatestTimestampResponseInfo = info as? LatestTimestampResponseInfo else {
 | |
|                         throw NetworkError.invalidResponse
 | |
|                     }
 | |
|                     
 | |
|                     return try response.validResultMap(
 | |
|                         sodium: sodium.wrappedValue,
 | |
|                         userX25519PublicKey: userX25519PublicKey,
 | |
|                         validationData: targetInfo.timestampMs
 | |
|                     )
 | |
|                 }
 | |
|                 .eraseToAnyPublisher()
 | |
|         }
 | |
|         catch { return Fail(error: error).eraseToAnyPublisher() }
 | |
|     }
 | |
|     
 | |
|     /// Clears all the user's data from their swarm. Returns a dictionary of snode public key to deletion confirmation.
 | |
|     public static func deleteAllMessages(
 | |
|         beforeMs: UInt64,
 | |
|         namespace: SnodeAPI.Namespace,
 | |
|         using dependencies: Dependencies = Dependencies()
 | |
|     ) -> AnyPublisher<[String: Bool], Error> {
 | |
|         guard let userED25519KeyPair = Identity.fetchUserEd25519KeyPair() else {
 | |
|             return Fail(error: SnodeAPIError.noKeyPair)
 | |
|                 .eraseToAnyPublisher()
 | |
|         }
 | |
|         
 | |
|         let userX25519PublicKey: String = getUserHexEncodedPublicKey()
 | |
|         
 | |
|         do {
 | |
|             return try SnodeAPI
 | |
|                 .prepareRequest(
 | |
|                     request: Request(
 | |
|                         endpoint: .deleteAllBefore,
 | |
|                         swarmPublicKey: userX25519PublicKey,
 | |
|                         requiresLatestNetworkTime: true,
 | |
|                         body: DeleteAllBeforeRequest(
 | |
|                             beforeMs: beforeMs,
 | |
|                             namespace: namespace,
 | |
|                             pubkey: userX25519PublicKey,
 | |
|                             timestampMs: UInt64(SnodeAPI.currentOffsetTimestampMs()),
 | |
|                             ed25519PublicKey: userED25519KeyPair.publicKey,
 | |
|                             ed25519SecretKey: userED25519KeyPair.secretKey
 | |
|                         )
 | |
|                     ),
 | |
|                     responseType: DeleteAllBeforeResponse.self,
 | |
|                     using: dependencies
 | |
|                 )
 | |
|                 .send(using: dependencies)
 | |
|                 .tryMap { _, response -> [String: Bool] in
 | |
|                     try response.validResultMap(
 | |
|                         sodium: sodium.wrappedValue,
 | |
|                         userX25519PublicKey: userX25519PublicKey,
 | |
|                         validationData: beforeMs
 | |
|                     )
 | |
|                 }
 | |
|                 .eraseToAnyPublisher()
 | |
|         }
 | |
|         catch { return Fail(error: error).eraseToAnyPublisher() }
 | |
|     }
 | |
|     
 | |
|     // MARK: - Internal API
 | |
|     
 | |
|     public static func getNetworkTime(
 | |
|         from snode: LibSession.Snode,
 | |
|         using dependencies: Dependencies
 | |
|     ) -> AnyPublisher<UInt64, Error> {
 | |
|         do {
 | |
|             return try SnodeAPI
 | |
|                 .prepareRequest(
 | |
|                     request: Request(
 | |
|                         endpoint: .getInfo,
 | |
|                         snode: snode,
 | |
|                         body: [String: String]()
 | |
|                     ),
 | |
|                     responseType: GetNetworkTimestampResponse.self,
 | |
|                     using: dependencies
 | |
|                 )
 | |
|                 .send(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
 | |
|                 }
 | |
|                 .eraseToAnyPublisher()
 | |
|         }
 | |
|         catch { return Fail(error: error).eraseToAnyPublisher() }
 | |
|     }
 | |
|     
 | |
|     // MARK: - Convenience
 | |
|     
 | |
|     private static func prepareRequest<T: Encodable, R: Decodable>(
 | |
|         request: Request<T, Endpoint>,
 | |
|         responseType: R.Type,
 | |
|         requireAllBatchResponses: Bool = true,
 | |
|         retryCount: Int = 0,
 | |
|         timeout: TimeInterval = Network.defaultTimeout,
 | |
|         using dependencies: Dependencies
 | |
|     ) throws -> Network.PreparedRequest<R> {
 | |
|         return Network.PreparedRequest<R>(
 | |
|             request: request,
 | |
|             urlRequest: try request.generateUrlRequest(using: dependencies),
 | |
|             responseType: responseType,
 | |
|             requireAllBatchResponses: requireAllBatchResponses,
 | |
|             retryCount: retryCount,
 | |
|             timeout: timeout
 | |
|         )
 | |
|         .handleEvents(
 | |
|             receiveOutput: { _, response in
 | |
|                 switch response {
 | |
|                     case let snodeResponse as SnodeResponse:
 | |
|                         // Update the network offset based on the response so subsequent requests have
 | |
|                         // the correct network offset time
 | |
|                         let offset = (Int64(snodeResponse.timeOffset) - Int64(floor(dependencies.dateNow.timeIntervalSince1970 * 1000)))
 | |
|                         SnodeAPI.clockOffsetMs.mutate { $0 = offset }
 | |
|                         
 | |
|                         // Extract and store hard fork information if returned
 | |
|                         guard snodeResponse.hardFork.count > 1 else { break }
 | |
|                         
 | |
|                         if snodeResponse.hardFork[1] > softfork {
 | |
|                             softfork = snodeResponse.hardFork[1]
 | |
|                             UserDefaults.standard[.softfork] = softfork
 | |
|                         }
 | |
|                         
 | |
|                         if snodeResponse.hardFork[0] > hardfork {
 | |
|                             hardfork = snodeResponse.hardFork[0]
 | |
|                             UserDefaults.standard[.hardfork] = hardfork
 | |
|                             softfork = snodeResponse.hardFork[1]
 | |
|                             UserDefaults.standard[.softfork] = softfork
 | |
|                         }
 | |
|                         
 | |
|                     default: break
 | |
|                 }
 | |
|             }
 | |
|         )
 | |
|     }
 | |
| }
 | |
| 
 | |
| // MARK: - Publisher Convenience
 | |
| 
 | |
| public extension Publisher where Output == Set<LibSession.Snode> {
 | |
|     func tryFlatMapWithRandomSnode<T, P>(
 | |
|         maxPublishers: Subscribers.Demand = .unlimited,
 | |
|         retry retries: Int = 0,
 | |
|         drainBehaviour: Atomic<SwarmDrainBehaviour> = .alwaysRandom,
 | |
|         using dependencies: Dependencies,
 | |
|         _ transform: @escaping (LibSession.Snode) throws -> P
 | |
|     ) -> AnyPublisher<T, Error> where T == P.Output, P: Publisher, P.Failure == Error {
 | |
|         return self
 | |
|             .mapError { $0 }
 | |
|             .flatMap(maxPublishers: maxPublishers) { swarm -> AnyPublisher<T, Error> in
 | |
|                 // If we don't want to reuse a specific snode multiple times then just grab a
 | |
|                 // random one from the swarm every time
 | |
|                 var remainingSnodes: Set<LibSession.Snode> = {
 | |
|                     switch drainBehaviour.wrappedValue {
 | |
|                         case .alwaysRandom: return swarm
 | |
|                         case .limitedReuse(_, let targetSnode, _, let usedSnodes, let swarmHash):
 | |
|                             // If we've used all of the snodes or the swarm has changed then reset the used list
 | |
|                             guard swarmHash == swarm.hashValue && (targetSnode != nil || usedSnodes != swarm) else {
 | |
|                                 drainBehaviour.mutate { $0 = $0.reset() }
 | |
|                                 return swarm
 | |
|                             }
 | |
|                             
 | |
|                             return swarm.subtracting(usedSnodes)
 | |
|                     }
 | |
|                 }()
 | |
|                 var lastError: Error?
 | |
|                 
 | |
|                 return Just(())
 | |
|                     .setFailureType(to: Error.self)
 | |
|                     .tryFlatMap(maxPublishers: maxPublishers) { _ -> AnyPublisher<T, Error> in
 | |
|                         let snode: LibSession.Snode = try {
 | |
|                             switch drainBehaviour.wrappedValue {
 | |
|                                 case .limitedReuse(_, .some(let targetSnode), _, _, _): return targetSnode
 | |
|                                 default: break
 | |
|                             }
 | |
|                             
 | |
|                             // Select the next snode
 | |
|                             return try dependencies.popRandomElement(&remainingSnodes) ?? {
 | |
|                                 throw SnodeAPIError.ranOutOfRandomSnodes(lastError)
 | |
|                             }()
 | |
|                         }()
 | |
|                         drainBehaviour.mutate { $0 = $0.use(snode: snode, from: swarm) }
 | |
|                         
 | |
|                         return try transform(snode)
 | |
|                             .eraseToAnyPublisher()
 | |
|                     }
 | |
|                     .mapError { error in
 | |
|                         // Prevent nesting the 'ranOutOfRandomSnodes' errors
 | |
|                         switch error {
 | |
|                             case SnodeAPIError.ranOutOfRandomSnodes: break
 | |
|                             default: lastError = error
 | |
|                         }
 | |
|                         
 | |
|                         return error
 | |
|                     }
 | |
|                     .retry(retries)
 | |
|                     .eraseToAnyPublisher()
 | |
|             }
 | |
|             .eraseToAnyPublisher()
 | |
|     }
 | |
| }
 | |
| 
 | |
| // MARK: - Request<T, EndpointType> Convenience
 | |
| 
 | |
| private extension Request {
 | |
|     init<B: Encodable>(
 | |
|         endpoint: SnodeAPI.Endpoint,
 | |
|         swarmPublicKey: String,
 | |
|         body: B,
 | |
|         retryCount: Int = SnodeAPI.maxRetryCount
 | |
|     ) where T == SnodeRequest<B>, Endpoint == SnodeAPI.Endpoint {
 | |
|         self = Request(
 | |
|             method: .post,
 | |
|             endpoint: endpoint,
 | |
|             swarmPublicKey: swarmPublicKey,
 | |
|             body: SnodeRequest<B>(
 | |
|                 endpoint: endpoint,
 | |
|                 body: body
 | |
|             ),
 | |
|             retryCount: retryCount
 | |
|         )
 | |
|     }
 | |
|     
 | |
|     init<B: Encodable>(
 | |
|         endpoint: SnodeAPI.Endpoint,
 | |
|         snode: LibSession.Snode,
 | |
|         swarmPublicKey: String? = nil,
 | |
|         body: B,
 | |
|         retryCount: Int = SnodeAPI.maxRetryCount
 | |
|     ) where T == SnodeRequest<B>, Endpoint == SnodeAPI.Endpoint {
 | |
|         self = Request(
 | |
|             method: .post,
 | |
|             endpoint: endpoint,
 | |
|             snode: snode,
 | |
|             body: SnodeRequest<B>(
 | |
|                 endpoint: endpoint,
 | |
|                 body: body
 | |
|             ),
 | |
|             swarmPublicKey: swarmPublicKey,
 | |
|             retryCount: retryCount
 | |
|         )
 | |
|     }
 | |
|     
 | |
|     init<B>(
 | |
|         endpoint: SnodeAPI.Endpoint,
 | |
|         swarmPublicKey: String,
 | |
|         requiresLatestNetworkTime: Bool,
 | |
|         body: B,
 | |
|         retryCount: Int = SnodeAPI.maxRetryCount
 | |
|     ) where T == SnodeRequest<B>, Endpoint == SnodeAPI.Endpoint, B: Encodable & UpdatableTimestamp {
 | |
|         self = Request(
 | |
|             method: .post,
 | |
|             endpoint: endpoint,
 | |
|             swarmPublicKey: swarmPublicKey,
 | |
|             requiresLatestNetworkTime: requiresLatestNetworkTime,
 | |
|             body: SnodeRequest<B>(
 | |
|                 endpoint: endpoint,
 | |
|                 body: body
 | |
|             ),
 | |
|             retryCount: retryCount
 | |
|         )
 | |
|     }
 | |
| }
 |