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.
		
		
		
		
		
			
		
			
				
	
	
		
			175 lines
		
	
	
		
			6.7 KiB
		
	
	
	
		
			Swift
		
	
			
		
		
	
	
			175 lines
		
	
	
		
			6.7 KiB
		
	
	
	
		
			Swift
		
	
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
 | 
						|
 | 
						|
import Foundation
 | 
						|
import PromiseKit
 | 
						|
import SessionUtilitiesKit
 | 
						|
import SessionSnodeKit
 | 
						|
 | 
						|
extension OpenGroupAPI {
 | 
						|
    // MARK: - BatchSubRequest
 | 
						|
    
 | 
						|
    struct BatchSubRequest: Encodable {
 | 
						|
        enum CodingKeys: String, CodingKey {
 | 
						|
            case method
 | 
						|
            case path
 | 
						|
            case headers
 | 
						|
            case json
 | 
						|
            case b64
 | 
						|
            case bytes
 | 
						|
        }
 | 
						|
        
 | 
						|
        let method: HTTP.Verb
 | 
						|
        let path: String
 | 
						|
        let headers: [String: String]?
 | 
						|
        
 | 
						|
        /// The `jsonBodyEncoder` is used to avoid having to make `BatchSubRequest` a generic type (haven't found a good way
 | 
						|
        /// to keep `BatchSubRequest` encodable using protocols unfortunately so need this work around)
 | 
						|
        private let jsonBodyEncoder: ((inout KeyedEncodingContainer<CodingKeys>, CodingKeys) throws -> ())?
 | 
						|
        private let b64: String?
 | 
						|
        private let bytes: [UInt8]?
 | 
						|
        
 | 
						|
        init<T: Encodable>(request: Request<T, Endpoint>) {
 | 
						|
            self.method = request.method
 | 
						|
            self.path = request.urlPathAndParamsString
 | 
						|
            self.headers = (request.headers.isEmpty ? nil : request.headers.toHTTPHeaders())
 | 
						|
            
 | 
						|
            // Note: Need to differentiate between JSON, b64 string and bytes body values to ensure they are
 | 
						|
            // encoded correctly so the server knows how to handle them
 | 
						|
            switch request.body {
 | 
						|
                case let bodyString as String:
 | 
						|
                    self.jsonBodyEncoder = nil
 | 
						|
                    self.b64 = bodyString
 | 
						|
                    self.bytes = nil
 | 
						|
                    
 | 
						|
                case let bodyBytes as [UInt8]:
 | 
						|
                    self.jsonBodyEncoder = nil
 | 
						|
                    self.b64 = nil
 | 
						|
                    self.bytes = bodyBytes
 | 
						|
                    
 | 
						|
                default:
 | 
						|
                    self.jsonBodyEncoder = { [body = request.body] container, key in
 | 
						|
                        try container.encodeIfPresent(body, forKey: key)
 | 
						|
                    }
 | 
						|
                    self.b64 = nil
 | 
						|
                    self.bytes = nil
 | 
						|
            }
 | 
						|
        }
 | 
						|
        
 | 
						|
        func encode(to encoder: Encoder) throws {
 | 
						|
            var container: KeyedEncodingContainer<CodingKeys> = encoder.container(keyedBy: CodingKeys.self)
 | 
						|
 | 
						|
            try container.encode(method, forKey: .method)
 | 
						|
            try container.encode(path, forKey: .path)
 | 
						|
            try container.encodeIfPresent(headers, forKey: .headers)
 | 
						|
            try jsonBodyEncoder?(&container, .json)
 | 
						|
            try container.encodeIfPresent(b64, forKey: .b64)
 | 
						|
            try container.encodeIfPresent(bytes, forKey: .bytes)
 | 
						|
        }
 | 
						|
    }
 | 
						|
    
 | 
						|
    // MARK: - BatchSubResponse<T>
 | 
						|
    
 | 
						|
    struct BatchSubResponse<T: Codable>: Codable {
 | 
						|
        /// The numeric http response code (e.g. 200 for success)
 | 
						|
        let code: Int32
 | 
						|
        
 | 
						|
        /// This should always include the content type of the request
 | 
						|
        let headers: [String: String]
 | 
						|
        
 | 
						|
        /// The body of the request; will be plain json if content-type is `application/json`, otherwise it will be base64 encoded data
 | 
						|
        let body: T?
 | 
						|
        
 | 
						|
        /// A flag to indicate that there was a body but it failed to parse
 | 
						|
        let failedToParseBody: Bool
 | 
						|
    }
 | 
						|
    
 | 
						|
    // MARK: - BatchRequestInfo<T, R>
 | 
						|
    
 | 
						|
    struct BatchRequestInfo<T: Encodable>: BatchRequestInfoType {
 | 
						|
        let request: Request<T, Endpoint>
 | 
						|
        let responseType: Codable.Type
 | 
						|
        
 | 
						|
        var endpoint: Endpoint { request.endpoint }
 | 
						|
        
 | 
						|
        init<R: Codable>(request: Request<T, Endpoint>, responseType: R.Type) {
 | 
						|
            self.request = request
 | 
						|
            self.responseType = BatchSubResponse<R>.self
 | 
						|
        }
 | 
						|
        
 | 
						|
        init(request: Request<T, Endpoint>) {
 | 
						|
            self.init(
 | 
						|
                request: request,
 | 
						|
                responseType: NoResponse.self
 | 
						|
            )
 | 
						|
        }
 | 
						|
        
 | 
						|
        func toSubRequest() -> BatchSubRequest {
 | 
						|
            return BatchSubRequest(request: request)
 | 
						|
        }
 | 
						|
    }
 | 
						|
    
 | 
						|
    // MARK: - BatchRequest
 | 
						|
    
 | 
						|
    typealias BatchRequest = [BatchSubRequest]
 | 
						|
    typealias BatchResponseTypes = [Codable.Type]
 | 
						|
    typealias BatchResponse = [(OnionRequestResponseInfoType, Codable?)]
 | 
						|
}
 | 
						|
 | 
						|
extension OpenGroupAPI.BatchSubResponse {
 | 
						|
    init(from decoder: Decoder) throws {
 | 
						|
        let container: KeyedDecodingContainer<CodingKeys> = try decoder.container(keyedBy: CodingKeys.self)
 | 
						|
        let body: T? = try? container.decode(T.self, forKey: .body)
 | 
						|
        
 | 
						|
        self = OpenGroupAPI.BatchSubResponse(
 | 
						|
            code: try container.decode(Int32.self, forKey: .code),
 | 
						|
            headers: try container.decode([String: String].self, forKey: .headers),
 | 
						|
            body: body,
 | 
						|
            failedToParseBody: (body == nil && T.self != NoResponse.self && !(T.self is ExpressibleByNilLiteral.Type))
 | 
						|
        )
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
// MARK: - BatchRequestInfoType
 | 
						|
 | 
						|
/// This protocol is designed to erase the types from `BatchRequestInfo<T, R>` so multiple types can be used
 | 
						|
/// in arrays when doing `/batch` and `/sequence` requests
 | 
						|
protocol BatchRequestInfoType {
 | 
						|
    var responseType: Codable.Type { get }
 | 
						|
    var endpoint: OpenGroupAPI.Endpoint { get }
 | 
						|
    
 | 
						|
    func toSubRequest() -> OpenGroupAPI.BatchSubRequest
 | 
						|
}
 | 
						|
 | 
						|
// MARK: - Convenience
 | 
						|
 | 
						|
public extension Decodable {
 | 
						|
    static func decoded(from data: Data, using dependencies: Dependencies = Dependencies()) throws -> Self {
 | 
						|
        return try data.decoded(as: Self.self, using: dependencies)
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
extension Promise where T == (OnionRequestResponseInfoType, Data?) {
 | 
						|
    func decoded(as types: OpenGroupAPI.BatchResponseTypes, on queue: DispatchQueue? = nil, using dependencies: Dependencies = Dependencies()) -> Promise<OpenGroupAPI.BatchResponse> {
 | 
						|
        self.map(on: queue) { responseInfo, maybeData -> OpenGroupAPI.BatchResponse in
 | 
						|
            // Need to split the data into an array of data so each item can be Decoded correctly
 | 
						|
            guard let data: Data = maybeData else { throw HTTP.Error.parsingFailed }
 | 
						|
            guard let jsonObject: Any = try? JSONSerialization.jsonObject(with: data, options: [.fragmentsAllowed]) else {
 | 
						|
                throw HTTP.Error.parsingFailed
 | 
						|
            }
 | 
						|
            guard let anyArray: [Any] = jsonObject as? [Any] else { throw HTTP.Error.parsingFailed }
 | 
						|
            
 | 
						|
            let dataArray: [Data] = anyArray.compactMap { try? JSONSerialization.data(withJSONObject: $0) }
 | 
						|
            guard dataArray.count == types.count else { throw HTTP.Error.parsingFailed }
 | 
						|
            
 | 
						|
            do {
 | 
						|
                return try zip(dataArray, types)
 | 
						|
                    .map { data, type in try type.decoded(from: data, using: dependencies) }
 | 
						|
                    .map { data in (responseInfo, data) }
 | 
						|
            }
 | 
						|
            catch {
 | 
						|
                throw HTTP.Error.parsingFailed
 | 
						|
            }
 | 
						|
        }
 | 
						|
    }
 | 
						|
}
 |