// Drop-in Swift client library for the Friendship Tracker HTTP API. // // Save this file alongside your code as `FriendshipClient.swift` and use // the FriendshipClient class: // // let c = FriendshipClient(token: "pat_...") // let rows = try await c.accountList(opts: ListOpts(limit: 20, sort: "-created_at")) // let fresh = try await c.accountCreate(["name": "Example GmbH"]) // // Every endpoint exposed by the HTTP API is wrapped as an async method // on FriendshipClient. List methods take a ListOpts struct; get/update/delete // methods take the row id as their first argument. // // Provided as-is, with no warranty. Vendor freely; modify as needed. // Targets Swift 5.7+ on macOS 12 / iOS 15 / Linux (Foundation + // FoundationNetworking on Linux). No external packages required. // // DO NOT EDIT THIS FILE MANUALLY - re-download from the docs site. // Local edits will be overwritten by the once-per-day version check. import Foundation #if canImport(FoundationNetworking) import FoundationNetworking #endif public enum FriendshipClientConstants { public static let appSlug = "friendship" public static let appName = "Friendship Tracker" public static let moduleName = "friendship_client" public static let clientVersion = "0.3.13" public static let language = "swift" public static let defaultBase = "https://friendship-tracker.com" /// Per-type metadata baked at generation time. Parse with /// JSONSerialization if you need legal filters / sorts / max_limit /// per model without an extra round-trip. public static let typesJson = #"{"activity":{"ops":["list","read","create","update","delete"],"create_fields":["parent_id","kind","summary","description","occurred_at","location"],"update_fields":["kind","summary","description","occurred_at","location"],"allowed_filters":["data__parent_id","data__kind","status","is_archived","owned_by"],"allowed_sorts":["created_at","data__occurred_at"],"default_sort":"data__occurred_at","max_limit":200,"fields":[{"name":"kind","type":"enum","values":["meeting","call","email","message","event","other"]},{"name":"summary","type":"string","max_len":400},{"name":"location","type":"string","max_len":200},{"name":"parent_id","type":"string","max_len":64,"ref":{"type":"contact","owned":true,"optional":false}},{"name":"description","type":"string","max_len":4000},{"name":"occurred_at","type":"string","max_len":32}]},"contact":{"ops":["list","read","create","update","delete"],"create_fields":["name","nickname","pronouns","email","phone","secondary_email","secondary_phone","company","job_title","address_line","city","country","website","linkedin","twitter","birthday","anniversary","gender","how_we_met","food_prefs","allergies","last_contacted_at","stay_in_touch_frequency","stay_in_touch_topic","notes","tags","favorite","avatar_blob_id","color"],"update_fields":["name","nickname","pronouns","email","phone","secondary_email","secondary_phone","company","job_title","address_line","city","country","website","linkedin","twitter","birthday","anniversary","gender","how_we_met","food_prefs","allergies","last_contacted_at","stay_in_touch_frequency","stay_in_touch_topic","notes","tags","favorite","avatar_blob_id","color"],"allowed_filters":["data__name","data__email","data__company","data__city","data__country","data__favorite","data__tags","data__gender","data__stay_in_touch_frequency","status","is_archived","owned_by"],"allowed_sorts":["created_at","updated_at","data__name","data__company","data__last_contacted_at","data__birthday","data__stay_in_touch_frequency"],"default_sort":"data__name","max_limit":200,"fields":[{"name":"city","type":"string","max_len":120},{"name":"name","type":"string","max_len":200},{"name":"tags","type":"tags"},{"name":"color","type":"string","max_len":24},{"name":"email","type":"string","max_len":320},{"name":"notes","type":"string","max_len":8000},{"name":"phone","type":"string","max_len":64},{"name":"gender","type":"enum","values":["male","female","other","unspecified"]},{"name":"company","type":"string","max_len":200},{"name":"country","type":"string","max_len":120},{"name":"twitter","type":"url"},{"name":"website","type":"url"},{"name":"birthday","type":"string","max_len":32},{"name":"favorite","type":"bool"},{"name":"linkedin","type":"url"},{"name":"nickname","type":"string","max_len":120},{"name":"pronouns","type":"string","max_len":32},{"name":"allergies","type":"string","max_len":600},{"name":"job_title","type":"string","max_len":200},{"name":"food_prefs","type":"string","max_len":600},{"name":"how_we_met","type":"string","max_len":1000},{"name":"anniversary","type":"string","max_len":32},{"name":"address_line","type":"string","max_len":200},{"name":"avatar_blob_id","type":"string","max_len":64},{"name":"secondary_email","type":"string","max_len":320},{"name":"secondary_phone","type":"string","max_len":64},{"name":"last_contacted_at","type":"string","max_len":32},{"name":"stay_in_touch_topic","type":"string","max_len":400},{"name":"stay_in_touch_frequency","type":"enum","values":["never","weekly","biweekly","monthly","quarterly","yearly"]}]},"conversation":{"ops":["list","read","create","update","delete"],"create_fields":["parent_id","channel","summary","content","occurred_at","sentiment","duration_minutes"],"update_fields":["channel","summary","content","occurred_at","sentiment","duration_minutes"],"allowed_filters":["data__parent_id","data__channel","data__sentiment","status","is_archived","owned_by"],"allowed_sorts":["created_at","data__occurred_at"],"default_sort":"data__occurred_at","max_limit":200,"fields":[{"name":"channel","type":"enum","values":["call","sms","whatsapp","email","in_person","video","voice","letter","other"]},{"name":"content","type":"string","max_len":8000},{"name":"summary","type":"string","max_len":400},{"name":"parent_id","type":"string","max_len":64,"ref":{"type":"contact","owned":true,"optional":false}},{"name":"sentiment","type":"enum","values":["positive","neutral","negative"]},{"name":"occurred_at","type":"string","max_len":32},{"name":"duration_minutes","type":"number"}]},"custom_field":{"ops":["list","read","create","update","delete"],"create_fields":["parent_id","label","value","kind","icon"],"update_fields":["label","value","kind","icon"],"allowed_filters":["data__parent_id","data__kind","status","is_archived","owned_by"],"allowed_sorts":["created_at","data__label"],"default_sort":"created_at","max_limit":200,"fields":[{"name":"icon","type":"string","max_len":32},{"name":"kind","type":"enum","values":["text","number","date","url","bool"]},{"name":"label","type":"string","max_len":80},{"name":"value","type":"string","max_len":2000},{"name":"parent_id","type":"string","max_len":64,"ref":{"type":"contact","owned":true,"optional":false}}]},"gift":{"ops":["list","read","create","update","delete"],"create_fields":["parent_id","title","occasion","status","occurred_at","price","currency","url","notes"],"update_fields":["title","occasion","status","occurred_at","price","currency","url","notes"],"allowed_filters":["data__parent_id","data__status","data__occasion","status","is_archived","owned_by"],"allowed_sorts":["created_at","data__occurred_at","data__title"],"default_sort":"data__occurred_at","max_limit":200,"fields":[{"name":"url","type":"url"},{"name":"notes","type":"string","max_len":4000},{"name":"price","type":"number"},{"name":"title","type":"string","max_len":200},{"name":"status","type":"enum","values":["idea","given","received"]},{"name":"currency","type":"string","max_len":8},{"name":"occasion","type":"string","max_len":120},{"name":"parent_id","type":"string","max_len":64,"ref":{"type":"contact","owned":true,"optional":false}},{"name":"occurred_at","type":"string","max_len":32}]},"journal_entry":{"ops":["list","read","create","update","delete"],"create_fields":["title","body","mood","occurred_at","tags"],"update_fields":["title","body","mood","occurred_at","tags"],"allowed_filters":["data__mood","data__tags","status","is_archived","owned_by"],"allowed_sorts":["data__occurred_at","created_at","updated_at"],"default_sort":"data__occurred_at","max_limit":200,"fields":[{"name":"body","type":"string","max_len":16000},{"name":"mood","type":"enum","values":["great","good","ok","down","awful"]},{"name":"tags","type":"tags"},{"name":"title","type":"string","max_len":200},{"name":"occurred_at","type":"string","max_len":32}]},"life_event":{"ops":["list","read","create","update","delete"],"create_fields":["parent_id","kind","title","occurred_at","description","location","recurring"],"update_fields":["kind","title","occurred_at","description","location","recurring"],"allowed_filters":["data__parent_id","data__kind","data__recurring","status","is_archived","owned_by"],"allowed_sorts":["data__occurred_at","created_at"],"default_sort":"data__occurred_at","max_limit":200,"fields":[{"name":"kind","type":"enum","values":["birthday","anniversary","met","graduation","job_start","job_end","move","marriage","birth","loss","milestone","custom"]},{"name":"title","type":"string","max_len":200},{"name":"location","type":"string","max_len":200},{"name":"parent_id","type":"string","max_len":64,"ref":{"type":"contact","owned":true,"optional":false}},{"name":"recurring","type":"bool"},{"name":"description","type":"string","max_len":4000},{"name":"occurred_at","type":"string","max_len":32}]},"note":{"ops":["list","read","create","update","delete"],"create_fields":["parent_id","body","pinned"],"update_fields":["body","pinned"],"allowed_filters":["data__parent_id","data__pinned","status","is_archived","owned_by"],"allowed_sorts":["created_at","updated_at"],"default_sort":"created_at","max_limit":200,"fields":[{"name":"body","type":"string","max_len":8000},{"name":"pinned","type":"bool"},{"name":"parent_id","type":"string","max_len":64,"ref":{"type":"contact","owned":true,"optional":false}}]},"pet":{"ops":["list","read","create","update","delete"],"create_fields":["parent_id","name","species","species_other","breed","born_at","color","notes"],"update_fields":["name","species","species_other","breed","born_at","color","notes"],"allowed_filters":["data__parent_id","data__species","status","is_archived","owned_by"],"allowed_sorts":["created_at","data__name","data__born_at"],"default_sort":"data__name","max_limit":200,"fields":[{"name":"name","type":"string","max_len":80},{"name":"breed","type":"string","max_len":120},{"name":"color","type":"string","max_len":80},{"name":"notes","type":"string","max_len":2000},{"name":"born_at","type":"string","max_len":32},{"name":"species","type":"enum","values":["dog","cat","bird","fish","rabbit","hamster","guinea_pig","reptile","horse","other"]},{"name":"parent_id","type":"string","max_len":64,"ref":{"type":"contact","owned":true,"optional":false}},{"name":"species_other","type":"string","max_len":80}]},"relationship":{"ops":["list","read","create","update","delete"],"create_fields":["parent_id","target_id","kind","label","since","notes"],"update_fields":["kind","label","since","notes"],"allowed_filters":["data__parent_id","data__target_id","data__kind","status","is_archived","owned_by"],"allowed_sorts":["created_at","data__kind"],"default_sort":"data__kind","max_limit":200,"fields":[{"name":"kind","type":"enum","values":["partner","spouse","parent","child","sibling","friend","colleague","manager","report","mentor","mentee","other"]},{"name":"label","type":"string","max_len":80},{"name":"notes","type":"string","max_len":2000},{"name":"since","type":"string","max_len":32},{"name":"parent_id","type":"string","max_len":64,"ref":{"type":"contact","owned":true,"optional":false}},{"name":"target_id","type":"string","max_len":64,"ref":{"type":"contact","owned":true,"optional":false}}]},"reminder":{"ops":["list","read","create","update","delete"],"create_fields":["parent_id","message","due_date","completed"],"update_fields":["message","due_date","completed"],"allowed_filters":["data__parent_id","data__completed","status","is_archived","owned_by"],"allowed_sorts":["created_at","data__due_date"],"default_sort":"data__due_date","max_limit":200,"fields":[{"name":"message","type":"string","max_len":400},{"name":"due_date","type":"string","max_len":32},{"name":"completed","type":"bool"},{"name":"parent_id","type":"string","max_len":64,"ref":{"type":"contact","owned":true,"optional":false}}]}}"# } public struct ApiError: Error, CustomStringConvertible { public let status: Int public let message: String public let bodyRaw: Any? public init(status: Int, message: String, body: Any? = nil) { self.status = status self.message = message self.bodyRaw = body } public var description: String { "HTTP \(status): \(message)" } } public struct ListOpts { public var limit: Int? public var offset: Int? public var sort: String? public var q: String? public var filters: [String: Any]? public init(limit: Int? = nil, offset: Int? = nil, sort: String? = nil, q: String? = nil, filters: [String: Any]? = nil) { self.limit = limit; self.offset = offset; self.sort = sort; self.q = q; self.filters = filters } } public final class FriendshipClient: @unchecked Sendable { // ── Configuration ───────────────────────────────────────────── private var baseUrl: String private var token: String private let deviceId: String private let sessionId: String private let session: URLSession private static var metaSentOnce = false private static var autoupdateTried = false private static let stateLock = NSLock() private static let retryable: Set = [408, 425, 429, 500, 502, 503, 504] private static let maxRetries = 3 private static let defaultTimeout: TimeInterval = 30 public init(token: String? = nil, baseUrl: String? = nil) { let envBase = ProcessInfo.processInfo.environment["XCLIENT_BASE_URL"] let chosenBase = (baseUrl?.isEmpty == false ? baseUrl : nil) ?? (envBase?.isEmpty == false ? envBase : nil) ?? FriendshipClientConstants.defaultBase self.baseUrl = FriendshipClient.trimTrailingSlash(chosenBase) if let t = token, !t.isEmpty { self.token = t } else { self.token = ProcessInfo.processInfo.environment["XCLIENT_TOKEN"] ?? "" } // Manual redirect handling - URLSession's default re-uses every // header on cross-origin hops, which would otherwise leak the // bearer token through a misconfigured proxy. let cfg = URLSessionConfiguration.ephemeral cfg.timeoutIntervalForRequest = FriendshipClient.defaultTimeout cfg.timeoutIntervalForResource = FriendshipClient.defaultTimeout self.session = URLSession(configuration: cfg) self.deviceId = FriendshipClient.loadOrMintDeviceId() self.sessionId = UUID().uuidString } public func setToken(_ token: String?) { self.token = token ?? "" } public func setBaseUrl(_ baseUrl: String){ self.baseUrl = FriendshipClient.trimTrailingSlash(baseUrl) } private static func trimTrailingSlash(_ s: String) -> String { var v = s while v.hasSuffix("/") { v.removeLast() } return v } // ── Identifier persistence ──────────────────────────────────── private static func stateDir() -> URL? { let env = ProcessInfo.processInfo.environment let home = env["HOME"] ?? env["USERPROFILE"] guard let home, !home.isEmpty else { return nil } let url = URL(fileURLWithPath: home).appendingPathComponent(".\(FriendshipClientConstants.moduleName)", isDirectory: true) do { try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true, attributes: [.posixPermissions: 0o700]) return url } catch { return nil } } private static func loadOrMintDeviceId() -> String { guard let d = stateDir() else { return UUID().uuidString } let f = d.appendingPathComponent("device.json") if let data = try? Data(contentsOf: f), let obj = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any], let did = obj["device_id"] as? String, did.count >= 32 { return did } let fresh = UUID().uuidString if let data = try? JSONSerialization.data(withJSONObject: ["device_id": fresh], options: []) { try? data.write(to: f) } return fresh } private static func autoupdateEnabled() -> Bool { let v = (ProcessInfo.processInfo.environment["XCLIENT_NO_AUTOUPDATE"] ?? "").lowercased() return !["1", "true", "yes"].contains(v) } // ── Editor / runtime fingerprint ────────────────────────────── private static func fingerprint() -> [String: Any] { let env = ProcessInfo.processInfo.environment let tp = (env["TERM_PROGRAM"] ?? "").lowercased() var out: [String: Any] = [ "swift_version": "5", "os": ProcessInfo.processInfo.operatingSystemVersionString, "ci": (env["CI"] != nil) || (env["GITHUB_ACTIONS"] != nil), "claude_code": (env["CLAUDECODE"] != nil) || (env["CLAUDE_CODE_ENTRYPOINT"] != nil), "codex": env["CODEX_HOME"] != nil, "vscode": tp == "vscode" && env["CURSOR_TRACE_ID"] == nil, "cursor": env["CURSOR_TRACE_ID"] != nil, "antigravity": env["ANTIGRAVITY_TRACE_ID"] != nil, "jetbrains": tp.contains("jetbrains"), ] if let v = env["TERM_PROGRAM"] { out["term_program"] = v } if let v = env["EDITOR"] { out["editor_env"] = v } return out } // ── HTTP transport ──────────────────────────────────────────── public func requestList(_ path: String, opts: ListOpts? = nil) async throws -> [String: Any]? { var qs = "" if let o = opts { func append(_ k: String, _ v: String) { if !qs.isEmpty { qs += "&" } qs += FriendshipClient.percentEncode(k) + "=" + FriendshipClient.percentEncode(v) } if let v = o.limit { append("limit", String(v)) } if let v = o.offset { append("offset", String(v)) } if let v = o.sort { append("sort", v) } if let v = o.q { append("q", v) } if let f = o.filters { for (k, v) in f { append(k, "\(v)") } } } var full = path if !qs.isEmpty { full += (path.contains("?") ? "&" : "?") + qs } return try await requestJson("GET", full, body: nil) } public func requestJson(_ method: String, _ path: String, body: Any?) async throws -> [String: Any]? { maybeAutoupdate() let url = baseUrl + path let json: Data? = try body.map { try JSONSerialization.data(withJSONObject: $0, options: []) } var lastErr: Error? = nil for attempt in 0..= 400 { let msg: String = { if let p = parsed as? [String: Any] { if let d = p["detail"] as? String { return d } if let d = p["message"] as? String { return d } } return "request failed" }() emitCallEvent(method, path: path, status: status, ok: false) throw ApiError(status: status, message: msg, body: parsed) } emitCallEvent(method, path: path, status: status, ok: true) return parsed as? [String: Any] } catch let e as ApiError { throw e } catch { lastErr = error if attempt + 1 < FriendshipClient.maxRetries { try? await FriendshipClient.sleep(FriendshipClient.backoffSeconds(attempt, retryAfter: nil)) continue } emitCallEvent(method, path: path, status: 0, ok: false) throw ApiError(status: 0, message: error.localizedDescription) } } emitCallEvent(method, path: path, status: 0, ok: false) throw ApiError(status: 0, message: lastErr?.localizedDescription ?? "request failed") } /// Walk redirects manually so Authorization can be stripped on /// cross-origin hops. Caps at 5 hops; mirrors RFC 7231 method /// rewrite semantics. URLSession's default redirect handling keeps /// every header, so we explicitly opt out via the delegate sleeve. private func sendFollowingRedirects(_ method: String, url: String, body: Data?) async throws -> (Data, HTTPURLResponse, [String: String]) { var currentUrl = url var currentMethod = method var currentBody = body var stripAuth = false let maxHops = 5 for hop in 0...maxHops { guard let u = URL(string: currentUrl) else { throw ApiError(status: 0, message: "invalid url") } var req = URLRequest(url: u) req.httpMethod = currentMethod req.timeoutInterval = FriendshipClient.defaultTimeout req.setValue("application/json", forHTTPHeaderField: "Accept") req.setValue(userAgent(), forHTTPHeaderField: "User-Agent") req.setValue("client_\(FriendshipClientConstants.language)", forHTTPHeaderField: "X-Client-Channel") req.setValue(FriendshipClientConstants.clientVersion, forHTTPHeaderField: "X-Client-Version") req.setValue(deviceId, forHTTPHeaderField: "X-Analytics-Device-Id") req.setValue(sessionId, forHTTPHeaderField: "X-Analytics-Session-Id") if !stripAuth, !token.isEmpty { req.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") } if let bodyData = currentBody, currentMethod != "GET", currentMethod != "HEAD" { req.setValue("application/json", forHTTPHeaderField: "Content-Type") req.httpBody = bodyData } let (data, response) = try await FriendshipClient.dataTask(session: session, request: req) guard let http = response as? HTTPURLResponse else { throw ApiError(status: 0, message: "non-http response") } var headers: [String: String] = [:] for (k, v) in http.allHeaderFields { if let kk = k as? String, let vv = v as? String { headers[kk.lowercased()] = vv } } let status = http.statusCode if status < 300 || status >= 400 || status == 304 || hop == maxHops { return (data, http, headers) } guard let loc = headers["location"], !loc.isEmpty else { return (data, http, headers) } let nextUrl: URL? = { if loc.lowercased().hasPrefix("http://") || loc.lowercased().hasPrefix("https://") { return URL(string: loc) } return URL(string: loc, relativeTo: u)?.absoluteURL }() guard let next = nextUrl else { return (data, http, headers) } if FriendshipClient.originOf(next) != FriendshipClient.originOf(u) { stripAuth = true } if status == 303 || ((status == 301 || status == 302) && currentMethod != "GET" && currentMethod != "HEAD") { currentMethod = "GET" currentBody = nil } currentUrl = next.absoluteString } throw ApiError(status: 0, message: "too many redirects") } /// Compatibility shim: `URLSession.data(for:)` exists on macOS 12+ /// / iOS 15+ but not on older Linux Foundation builds. Wrap the /// completion-handler API so this compiles on every supported /// runtime. private static func dataTask(session: URLSession, request: URLRequest) async throws -> (Data, URLResponse) { try await withCheckedThrowingContinuation { (cont: CheckedContinuation<(Data, URLResponse), Error>) in let task = session.dataTask(with: request) { data, response, error in if let error = error { cont.resume(throwing: error); return } guard let data = data, let response = response else { cont.resume(throwing: ApiError(status: 0, message: "empty response")) return } cont.resume(returning: (data, response)) } task.resume() } } private static func backoffSeconds(_ attempt: Int, retryAfter: Double?) -> TimeInterval { if let r = retryAfter, r >= 0 { return min(r, 60.0) } return min(pow(2.0, Double(attempt)), 60.0) } private static func sleep(_ seconds: TimeInterval) async throws { try await Task.sleep(nanoseconds: UInt64(max(0, seconds) * 1_000_000_000)) } private static func originOf(_ u: URL) -> String { let scheme = (u.scheme ?? "").lowercased() let host = (u.host ?? "").lowercased() let port = u.port ?? (scheme == "https" ? 443 : 80) return "\(scheme)://\(host):\(port)" } private static func percentEncode(_ s: String) -> String { var allowed = CharacterSet.urlQueryAllowed allowed.remove(charactersIn: "&=+?#") return s.addingPercentEncoding(withAllowedCharacters: allowed) ?? s } private func userAgent() -> String { return "\(FriendshipClientConstants.moduleName)/\(FriendshipClientConstants.clientVersion) (lib/\(FriendshipClientConstants.language); swift)" } // ── Analytics ───────────────────────────────────────────────── private func emitCallEvent(_ method: String, path: String, status: Int, ok: Bool) { var includeEnv = false FriendshipClient.stateLock.lock() if !FriendshipClient.metaSentOnce { FriendshipClient.metaSentOnce = true includeEnv = true } FriendshipClient.stateLock.unlock() Task.detached(priority: .background) { do { var meta: [String: Any] = [ "channel": "client_\(FriendshipClientConstants.language)", "client_version": FriendshipClientConstants.clientVersion, "module_name": FriendshipClientConstants.moduleName, "language": FriendshipClientConstants.language, "os": ProcessInfo.processInfo.operatingSystemVersionString, ] if includeEnv { meta["env"] = FriendshipClient.fingerprint() } let evt: [String: Any] = [ "type": "client.call", "ts_client": Int(Date().timeIntervalSince1970), "meta": [ "method": method.uppercased(), "path": path.split(separator: "?", maxSplits: 1).first.map(String.init) ?? path, "status": status, "ok": ok, ], ] let body: [String: Any] = [ "device_id": self.deviceId, "session_id": self.sessionId, "events": [evt], "meta": meta, ] guard let url = URL(string: self.baseUrl + "/xapi2/analytics/challenge") else { return } var req = URLRequest(url: url) req.httpMethod = "POST" req.timeoutInterval = 4 req.setValue("application/json", forHTTPHeaderField: "Content-Type") req.setValue(self.userAgent(), forHTTPHeaderField: "User-Agent") req.httpBody = try JSONSerialization.data(withJSONObject: body, options: []) _ = try? await FriendshipClient.dataTask(session: self.session, request: req) } catch { /* fire and forget */ } } } // ── Auto-update ─────────────────────────────────────────────── private func maybeAutoupdate() { FriendshipClient.stateLock.lock() if FriendshipClient.autoupdateTried { FriendshipClient.stateLock.unlock() return } FriendshipClient.autoupdateTried = true FriendshipClient.stateLock.unlock() guard FriendshipClient.autoupdateEnabled() else { return } Task.detached(priority: .background) { // Source replacement is intentionally a no-op - the user // ships compiled artefacts. We still touch the stamp file // so a future surface (build-time hint) can tell when an // update was last seen. guard let d = FriendshipClient.stateDir() else { return } let stamp = d.appendingPathComponent("update_check.json") let now = Int(Date().timeIntervalSince1970) if let data = try? Data(contentsOf: stamp), let obj = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any], let last = obj["checked_at"] as? Int, now - last < 86400 { return } if let bytes = try? JSONSerialization.data(withJSONObject: ["checked_at": now], options: []) { try? bytes.write(to: stamp) } } } // ── Generated per-type wrapper methods ─────────────────────── // Every model that exposes an op gets one async `` // method below. The runtime above does the heavy lifting; these // wrappers just pin the URL + HTTP verb. /// List `activity` rows. public func activityList(opts: ListOpts? = nil) async throws -> [String: Any]? { return try await requestList("/xapi2/data/activity", opts: opts) } /// Fetch one `activity` row by id. public func activityGet(_ id: String) async throws -> [String: Any]? { return try await requestJson("GET", "/xapi2/data/activity/" + id, body: nil) } /// Create a new `activity` row. public func activityCreate(_ data: [String: Any]) async throws -> [String: Any]? { return try await requestJson("POST", "/xapi2/data/activity", body: data) } /// Patch a `activity` row. public func activityUpdate(_ id: String, _ data: [String: Any]) async throws -> [String: Any]? { return try await requestJson("PATCH", "/xapi2/data/activity/" + id, body: data) } /// Delete a `activity` row. @discardableResult public func activityDelete(_ id: String) async throws -> Bool { _ = try await requestJson("DELETE", "/xapi2/data/activity/" + id, body: nil) return true } /// List `contact` rows. public func contactList(opts: ListOpts? = nil) async throws -> [String: Any]? { return try await requestList("/xapi2/data/contact", opts: opts) } /// Fetch one `contact` row by id. public func contactGet(_ id: String) async throws -> [String: Any]? { return try await requestJson("GET", "/xapi2/data/contact/" + id, body: nil) } /// Create a new `contact` row. public func contactCreate(_ data: [String: Any]) async throws -> [String: Any]? { return try await requestJson("POST", "/xapi2/data/contact", body: data) } /// Patch a `contact` row. public func contactUpdate(_ id: String, _ data: [String: Any]) async throws -> [String: Any]? { return try await requestJson("PATCH", "/xapi2/data/contact/" + id, body: data) } /// Delete a `contact` row. @discardableResult public func contactDelete(_ id: String) async throws -> Bool { _ = try await requestJson("DELETE", "/xapi2/data/contact/" + id, body: nil) return true } /// List `conversation` rows. public func conversationList(opts: ListOpts? = nil) async throws -> [String: Any]? { return try await requestList("/xapi2/data/conversation", opts: opts) } /// Fetch one `conversation` row by id. public func conversationGet(_ id: String) async throws -> [String: Any]? { return try await requestJson("GET", "/xapi2/data/conversation/" + id, body: nil) } /// Create a new `conversation` row. public func conversationCreate(_ data: [String: Any]) async throws -> [String: Any]? { return try await requestJson("POST", "/xapi2/data/conversation", body: data) } /// Patch a `conversation` row. public func conversationUpdate(_ id: String, _ data: [String: Any]) async throws -> [String: Any]? { return try await requestJson("PATCH", "/xapi2/data/conversation/" + id, body: data) } /// Delete a `conversation` row. @discardableResult public func conversationDelete(_ id: String) async throws -> Bool { _ = try await requestJson("DELETE", "/xapi2/data/conversation/" + id, body: nil) return true } /// List `custom_field` rows. public func customFieldList(opts: ListOpts? = nil) async throws -> [String: Any]? { return try await requestList("/xapi2/data/custom_field", opts: opts) } /// Fetch one `custom_field` row by id. public func customFieldGet(_ id: String) async throws -> [String: Any]? { return try await requestJson("GET", "/xapi2/data/custom_field/" + id, body: nil) } /// Create a new `custom_field` row. public func customFieldCreate(_ data: [String: Any]) async throws -> [String: Any]? { return try await requestJson("POST", "/xapi2/data/custom_field", body: data) } /// Patch a `custom_field` row. public func customFieldUpdate(_ id: String, _ data: [String: Any]) async throws -> [String: Any]? { return try await requestJson("PATCH", "/xapi2/data/custom_field/" + id, body: data) } /// Delete a `custom_field` row. @discardableResult public func customFieldDelete(_ id: String) async throws -> Bool { _ = try await requestJson("DELETE", "/xapi2/data/custom_field/" + id, body: nil) return true } /// List `gift` rows. public func giftList(opts: ListOpts? = nil) async throws -> [String: Any]? { return try await requestList("/xapi2/data/gift", opts: opts) } /// Fetch one `gift` row by id. public func giftGet(_ id: String) async throws -> [String: Any]? { return try await requestJson("GET", "/xapi2/data/gift/" + id, body: nil) } /// Create a new `gift` row. public func giftCreate(_ data: [String: Any]) async throws -> [String: Any]? { return try await requestJson("POST", "/xapi2/data/gift", body: data) } /// Patch a `gift` row. public func giftUpdate(_ id: String, _ data: [String: Any]) async throws -> [String: Any]? { return try await requestJson("PATCH", "/xapi2/data/gift/" + id, body: data) } /// Delete a `gift` row. @discardableResult public func giftDelete(_ id: String) async throws -> Bool { _ = try await requestJson("DELETE", "/xapi2/data/gift/" + id, body: nil) return true } /// List `journal_entry` rows. public func journalEntryList(opts: ListOpts? = nil) async throws -> [String: Any]? { return try await requestList("/xapi2/data/journal_entry", opts: opts) } /// Fetch one `journal_entry` row by id. public func journalEntryGet(_ id: String) async throws -> [String: Any]? { return try await requestJson("GET", "/xapi2/data/journal_entry/" + id, body: nil) } /// Create a new `journal_entry` row. public func journalEntryCreate(_ data: [String: Any]) async throws -> [String: Any]? { return try await requestJson("POST", "/xapi2/data/journal_entry", body: data) } /// Patch a `journal_entry` row. public func journalEntryUpdate(_ id: String, _ data: [String: Any]) async throws -> [String: Any]? { return try await requestJson("PATCH", "/xapi2/data/journal_entry/" + id, body: data) } /// Delete a `journal_entry` row. @discardableResult public func journalEntryDelete(_ id: String) async throws -> Bool { _ = try await requestJson("DELETE", "/xapi2/data/journal_entry/" + id, body: nil) return true } /// List `life_event` rows. public func lifeEventList(opts: ListOpts? = nil) async throws -> [String: Any]? { return try await requestList("/xapi2/data/life_event", opts: opts) } /// Fetch one `life_event` row by id. public func lifeEventGet(_ id: String) async throws -> [String: Any]? { return try await requestJson("GET", "/xapi2/data/life_event/" + id, body: nil) } /// Create a new `life_event` row. public func lifeEventCreate(_ data: [String: Any]) async throws -> [String: Any]? { return try await requestJson("POST", "/xapi2/data/life_event", body: data) } /// Patch a `life_event` row. public func lifeEventUpdate(_ id: String, _ data: [String: Any]) async throws -> [String: Any]? { return try await requestJson("PATCH", "/xapi2/data/life_event/" + id, body: data) } /// Delete a `life_event` row. @discardableResult public func lifeEventDelete(_ id: String) async throws -> Bool { _ = try await requestJson("DELETE", "/xapi2/data/life_event/" + id, body: nil) return true } /// List `note` rows. public func noteList(opts: ListOpts? = nil) async throws -> [String: Any]? { return try await requestList("/xapi2/data/note", opts: opts) } /// Fetch one `note` row by id. public func noteGet(_ id: String) async throws -> [String: Any]? { return try await requestJson("GET", "/xapi2/data/note/" + id, body: nil) } /// Create a new `note` row. public func noteCreate(_ data: [String: Any]) async throws -> [String: Any]? { return try await requestJson("POST", "/xapi2/data/note", body: data) } /// Patch a `note` row. public func noteUpdate(_ id: String, _ data: [String: Any]) async throws -> [String: Any]? { return try await requestJson("PATCH", "/xapi2/data/note/" + id, body: data) } /// Delete a `note` row. @discardableResult public func noteDelete(_ id: String) async throws -> Bool { _ = try await requestJson("DELETE", "/xapi2/data/note/" + id, body: nil) return true } /// List `pet` rows. public func petList(opts: ListOpts? = nil) async throws -> [String: Any]? { return try await requestList("/xapi2/data/pet", opts: opts) } /// Fetch one `pet` row by id. public func petGet(_ id: String) async throws -> [String: Any]? { return try await requestJson("GET", "/xapi2/data/pet/" + id, body: nil) } /// Create a new `pet` row. public func petCreate(_ data: [String: Any]) async throws -> [String: Any]? { return try await requestJson("POST", "/xapi2/data/pet", body: data) } /// Patch a `pet` row. public func petUpdate(_ id: String, _ data: [String: Any]) async throws -> [String: Any]? { return try await requestJson("PATCH", "/xapi2/data/pet/" + id, body: data) } /// Delete a `pet` row. @discardableResult public func petDelete(_ id: String) async throws -> Bool { _ = try await requestJson("DELETE", "/xapi2/data/pet/" + id, body: nil) return true } /// List `relationship` rows. public func relationshipList(opts: ListOpts? = nil) async throws -> [String: Any]? { return try await requestList("/xapi2/data/relationship", opts: opts) } /// Fetch one `relationship` row by id. public func relationshipGet(_ id: String) async throws -> [String: Any]? { return try await requestJson("GET", "/xapi2/data/relationship/" + id, body: nil) } /// Create a new `relationship` row. public func relationshipCreate(_ data: [String: Any]) async throws -> [String: Any]? { return try await requestJson("POST", "/xapi2/data/relationship", body: data) } /// Patch a `relationship` row. public func relationshipUpdate(_ id: String, _ data: [String: Any]) async throws -> [String: Any]? { return try await requestJson("PATCH", "/xapi2/data/relationship/" + id, body: data) } /// Delete a `relationship` row. @discardableResult public func relationshipDelete(_ id: String) async throws -> Bool { _ = try await requestJson("DELETE", "/xapi2/data/relationship/" + id, body: nil) return true } /// List `reminder` rows. public func reminderList(opts: ListOpts? = nil) async throws -> [String: Any]? { return try await requestList("/xapi2/data/reminder", opts: opts) } /// Fetch one `reminder` row by id. public func reminderGet(_ id: String) async throws -> [String: Any]? { return try await requestJson("GET", "/xapi2/data/reminder/" + id, body: nil) } /// Create a new `reminder` row. public func reminderCreate(_ data: [String: Any]) async throws -> [String: Any]? { return try await requestJson("POST", "/xapi2/data/reminder", body: data) } /// Patch a `reminder` row. public func reminderUpdate(_ id: String, _ data: [String: Any]) async throws -> [String: Any]? { return try await requestJson("PATCH", "/xapi2/data/reminder/" + id, body: data) } /// Delete a `reminder` row. @discardableResult public func reminderDelete(_ id: String) async throws -> Bool { _ = try await requestJson("DELETE", "/xapi2/data/reminder/" + id, body: nil) return true } }