From cf587aa0573ac4f34e1effe70108a9eca82093ac Mon Sep 17 00:00:00 2001 From: Christian Cleberg Date: Tue, 24 Mar 2026 21:48:23 -0500 Subject: v1.0 --- Rune/API/NjallaClient.swift | 129 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 129 insertions(+) create mode 100644 Rune/API/NjallaClient.swift (limited to 'Rune/API/NjallaClient.swift') diff --git a/Rune/API/NjallaClient.swift b/Rune/API/NjallaClient.swift new file mode 100644 index 0000000..9158e20 --- /dev/null +++ b/Rune/API/NjallaClient.swift @@ -0,0 +1,129 @@ +import Foundation + +struct NjallaClient: Sendable, Equatable { + private let token: String + private let endpoint = URL(string: "https://njal.la/api/1/")! + + init(token: String) { + self.token = token + } + + func call(_ method: String, params: [String: Any] = [:]) async throws -> T { + var request = URLRequest(url: endpoint) + request.httpMethod = "POST" + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + request.setValue("application/json", forHTTPHeaderField: "Accept") + request.setValue("Njalla \(token)", forHTTPHeaderField: "Authorization") + + let body: [String: Any] = [ + "jsonrpc": "2.0", + "method": method, + "params": params, + "id": "1" + ] + + request.httpBody = try JSONSerialization.data(withJSONObject: body) + + let (data, response) = try await URLSession.shared.data(for: request) + let statusCode = (response as? HTTPURLResponse)?.statusCode ?? -1 + + #if DEBUG + debugPrint("Njalla method:", method) + debugPrint("Njalla status:", statusCode) + #endif + + let decoder = JSONDecoder() + let envelope = try decoder.decode(RPCResponse.self, from: data) + + if let error = envelope.error { + throw NjallaError.api(message: error.message) + } + + guard let result = envelope.result else { + throw NjallaError.missingResult + } + + return result + } + + func listDomains() async throws -> [Domain] { + let response: DomainListResponse = try await call("list-domains") + return response.domains + } + + func getDomain(named domain: String) async throws -> Domain { + try await call("get-domain", params: ["domain": domain]) + } + + func editDomain(named domain: String, request: DomainUpdateRequest) async throws -> Domain { + var params = request.params + params["domain"] = domain + return try await call("edit-domain", params: params) + } + + func listRecords(for domain: String) async throws -> [DNSRecord] { + let response: RecordListResponse = try await call("list-records", params: ["domain": domain]) + return response.records.map { record in + record.domain.isEmpty ? record.withDomain(domain) : record + } + } + + func addRecord(for domain: String, draft: DNSRecordDraft) async throws -> DNSRecord { + try await call("add-record", params: draft.params(domain: domain)) + } + + func editRecord(for domain: String, id: String, draft: DNSRecordDraft) async throws -> DNSRecord { + try await call("edit-record", params: draft.params(domain: domain, id: id)) + } + + func removeRecord(_ record: DNSRecord) async throws { + let params: [String: Any] = [ + "domain": record.domain, + "id": record.id, + "name": record.name, + "type": record.type + ] + let _: RecordListResponse = try await call("remove-record", params: params) + } + + func listTokens() async throws -> [APIToken] { + let response: TokenListResponse = try await call("list-tokens") + return response.tokens + } + + func addToken(request: TokenCreateRequest) async throws { + let _: EmptyResult = try await call("add-token", params: request.params) + } + + func removeToken(key: String) async throws { + let _: EmptyResult = try await call("remove-token", params: ["key": key]) + } + + func getBalance() async throws -> WalletBalance { + try await call("get-balance") + } +} + +enum NjallaError: LocalizedError { + case api(message: String) + case missingResult + + var errorDescription: String? { + switch self { + case .api(let message): + return message + case .missingResult: + return "The API response did not include a result." + } + } +} + +private struct RPCResponse: Decodable { + let result: Result? + let error: RPCError? +} + +private struct RPCError: Decodable { + let code: Int + let message: String +} -- cgit v1.2.3