// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. import Foundation import GRDB import SessionUtilitiesKit public struct Snode: Codable, FetchableRecord, PersistableRecord, TableRecord, ColumnExpressible, Hashable, CustomStringConvertible { public static var databaseTableName: String { "snode" } static let snodeSet = hasMany(SnodeSet.self) static let snodeSetForeignKey = ForeignKey( [Columns.address, Columns.port], to: [SnodeSet.Columns.address, SnodeSet.Columns.port] ) public typealias Columns = CodingKeys public enum CodingKeys: String, CodingKey, ColumnExpression { case address = "public_ip" case port = "storage_port" case ed25519PublicKey = "pubkey_ed25519" case x25519PublicKey = "pubkey_x25519" } public let address: String public let port: UInt16 public let ed25519PublicKey: String public let x25519PublicKey: String public var ip: String { guard let range = address.range(of: "https://"), range.lowerBound == address.startIndex else { return address } return String(address[range.upperBound.. { request(for: Snode.snodeSet) } public var description: String { return "\(address):\(port)" } } // MARK: - Decoder extension Snode { public init(from decoder: Decoder) throws { let container: KeyedDecodingContainer = try decoder.container(keyedBy: CodingKeys.self) do { let address: String = try container.decode(String.self, forKey: .address) guard address != "0.0.0.0" else { throw SnodeAPIError.invalidIP } self = Snode( address: (address.starts(with: "https://") ? address : "https://\(address)"), port: try container.decode(UInt16.self, forKey: .port), ed25519PublicKey: try container.decode(String.self, forKey: .ed25519PublicKey), x25519PublicKey: try container.decode(String.self, forKey: .x25519PublicKey) ) } catch { SNLog("Failed to parse snode: \(error.localizedDescription).") throw HTTPError.invalidJSON } } } // MARK: - GRDB Interactions internal extension Snode { static func fetchSet(_ db: Database, publicKey: String) throws -> Set { return try Snode .joining( required: Snode.snodeSet .filter(SnodeSet.Columns.key == publicKey) .order(SnodeSet.Columns.nodeIndex) ) .fetchSet(db) } static func fetchAllOnionRequestPaths(_ db: Database) throws -> [[Snode]] { struct ResultWrapper: Decodable, FetchableRecord { let key: String let nodeIndex: Int let address: String let port: UInt16 let snode: Snode } return try SnodeSet .filter(SnodeSet.Columns.key.like("\(SnodeSet.onionRequestPathPrefix)%")) .order( SnodeSet.Columns.nodeIndex, SnodeSet.Columns.key ) .including(required: SnodeSet.node) .asRequest(of: ResultWrapper.self) .fetchAll(db) .reduce(into: [:]) { prev, next in // Reducing will lose the 'key' sorting prev[next.key] = (prev[next.key] ?? []).appending(next.snode) } .asArray() .sorted(by: { lhs, rhs in lhs.key < rhs.key }) .compactMap { _, nodes in !nodes.isEmpty ? nodes : nil } // Exclude empty sets } static func clearOnionRequestPaths(_ db: Database) throws { try SnodeSet .filter(SnodeSet.Columns.key.like("\(SnodeSet.onionRequestPathPrefix)%")) .deleteAll(db) } } internal extension Collection where Element == Snode { /// This method is used to save Swarms func save(_ db: Database, key: String) throws { try self.enumerated().forEach { nodeIndex, node in try node.save(db) try SnodeSet( key: key, nodeIndex: nodeIndex, address: node.address, port: node.port ).save(db) } } } internal extension Collection where Element == [Snode] { /// This method is used to save onion reuqest paths func save(_ db: Database) throws { try self.enumerated().forEach { pathIndex, path in try path.save(db, key: "\(SnodeSet.onionRequestPathPrefix)\(pathIndex)") } } }