summaryrefslogtreecommitdiff
path: root/Rune/API/NjallaClient.swift
diff options
context:
space:
mode:
authorChristian Cleberg <[email protected]>2026-03-24 21:48:23 -0500
committerChristian Cleberg <[email protected]>2026-03-24 21:48:23 -0500
commitcf587aa0573ac4f34e1effe70108a9eca82093ac (patch)
tree833e6f4961522e35b08af01b5f012983f8e34678 /Rune/API/NjallaClient.swift
downloadrune-1.0.0.tar.gz
rune-1.0.0.tar.bz2
rune-1.0.0.zip
v1.0v1.0.0
Diffstat (limited to 'Rune/API/NjallaClient.swift')
-rw-r--r--Rune/API/NjallaClient.swift129
1 files changed, 129 insertions, 0 deletions
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<T: Decodable>(_ 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<T>.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<Result: Decodable>: Decodable {
+ let result: Result?
+ let error: RPCError?
+}
+
+private struct RPCError: Decodable {
+ let code: Int
+ let message: String
+}