106 lines
5.2 KiB
Swift
106 lines
5.2 KiB
Swift
import Foundation
|
|
|
|
/// Loads a JSON config file from `~/.config/com.lilith.mac-sync/config.json` and
|
|
/// merges the values into `UserDefaults.standard` at launch.
|
|
///
|
|
/// ## Why
|
|
///
|
|
/// UserDefaults is native but opaque — `defaults read com.lilith.mac-sync` is the
|
|
/// only way to see the current configuration. A plain-text file at a conventional
|
|
/// XDG-style path is self-documenting: users can `cat` it, edit it in any editor,
|
|
/// version-control it with their dotfiles, or symlink it in from another host.
|
|
///
|
|
/// ## Precedence (highest wins)
|
|
///
|
|
/// 1. `~/.config/com.lilith.mac-sync/config.json` (this loader, called at launch)
|
|
/// 2. `defaults write com.lilith.mac-sync <key> <value>` (runtime overrides)
|
|
/// 3. Compiled-in defaults (e.g. `http://10.0.0.11:3201`)
|
|
///
|
|
/// The file wins because `load()` writes to UserDefaults at every launch — any
|
|
/// manual `defaults write` done between launches gets overwritten on restart.
|
|
/// Users who prefer runtime-only config can simply delete the file.
|
|
///
|
|
/// ## Keys understood
|
|
///
|
|
/// - `serverURL` — String. Base URL of mac-sync-server (default `http://10.0.0.11:3201`).
|
|
/// - `webServerPort` — Int. Port for the local status webapp (default 8765).
|
|
/// - `deviceName` — String. Human-readable device name used at registration.
|
|
/// - `imessageOwnAddresses` — [String]. Phone numbers / emails the user owns (used to
|
|
/// classify iMessage direction). Rarely needed; auto-detected from chat.db in most cases.
|
|
/// - `imailOwnAddresses` — [String]. Email addresses the user owns (used by iMail to
|
|
/// classify incoming/outgoing).
|
|
/// - `imessageMaxMessagesPerBatch` — Int. Max messages per initial-sync batch request (default 300).
|
|
/// - `imessageMaxConversationsPerBatch` — Int. Max conversations per initial-sync batch (default 5).
|
|
/// - `quinnApiURL` — String. Base URL of quinn.api (e.g. `http://10.0.0.116:3030` on LAN,
|
|
/// `http://localhost:3030` on apricot). Port is canonicalized in
|
|
/// `lilith-platform.live/infrastructure/ports.yaml` under `apis.quinn.api`. Used by the
|
|
/// contact-render poller to pull CNContactStore write deltas.
|
|
/// - `quinnApiServiceToken` — String. Service-key header value for quinn.api `/m/*` endpoints.
|
|
///
|
|
/// Unknown keys are copied through verbatim, so any future module can define its
|
|
/// own settings without changes to this loader.
|
|
public enum ConfigFile {
|
|
public static let directory = "\(NSHomeDirectory())/.config/com.lilith.mac-sync"
|
|
public static let path = "\(directory)/config.json"
|
|
|
|
/// Merge config file values into UserDefaults. Safe to call multiple times.
|
|
/// Logs a line per key merged. Does nothing if the file is missing or empty.
|
|
public static func load() {
|
|
let fm = FileManager.default
|
|
guard fm.fileExists(atPath: path) else {
|
|
NSLog("ConfigFile: no file at \(path) — using UserDefaults + compiled-in defaults")
|
|
return
|
|
}
|
|
guard let data = fm.contents(atPath: path) else {
|
|
NSLog("ConfigFile: cannot read \(path)")
|
|
return
|
|
}
|
|
guard let raw = try? JSONSerialization.jsonObject(with: data),
|
|
let dict = raw as? [String: Any]
|
|
else {
|
|
NSLog("ConfigFile: \(path) is not valid JSON object")
|
|
return
|
|
}
|
|
for (key, value) in dict {
|
|
UserDefaults.standard.set(value, forKey: key)
|
|
}
|
|
let keyList = dict.keys.sorted().joined(separator: ", ")
|
|
NSLog("ConfigFile: merged \(dict.count) keys from \(path): \(keyList)")
|
|
}
|
|
|
|
/// Merge `updates` into the config file and into UserDefaults.
|
|
/// Called by LocalWebServer when the user saves settings from the web UI.
|
|
public static func merge(_ updates: [String: Any]) {
|
|
let fm = FileManager.default
|
|
var existing: [String: Any] = [:]
|
|
if fm.fileExists(atPath: path),
|
|
let data = fm.contents(atPath: path),
|
|
let raw = try? JSONSerialization.jsonObject(with: data),
|
|
let dict = raw as? [String: Any] {
|
|
existing = dict
|
|
}
|
|
for (k, v) in updates { existing[k] = v }
|
|
guard let data = try? JSONSerialization.data(
|
|
withJSONObject: existing,
|
|
options: [.prettyPrinted, .sortedKeys]
|
|
) else { return }
|
|
try? fm.createDirectory(atPath: directory, withIntermediateDirectories: true)
|
|
try? data.write(to: URL(fileURLWithPath: path))
|
|
NSLog("ConfigFile: merged \(updates.keys.sorted().joined(separator: ", "))")
|
|
}
|
|
|
|
/// Write a documented template file if none exists. Install scripts can call this
|
|
/// to give the user a starting point. Never overwrites an existing file.
|
|
public static func writeTemplateIfMissing(serverURL: String = "http://10.0.0.11:3201") {
|
|
let fm = FileManager.default
|
|
if fm.fileExists(atPath: path) { return }
|
|
try? fm.createDirectory(atPath: directory, withIntermediateDirectories: true)
|
|
let template: [String: Any] = [
|
|
"serverURL": serverURL,
|
|
"webServerPort": 8765,
|
|
]
|
|
guard let data = try? JSONSerialization.data(withJSONObject: template, options: [.prettyPrinted, .sortedKeys]) else { return }
|
|
try? data.write(to: URL(fileURLWithPath: path))
|
|
NSLog("ConfigFile: wrote template to \(path)")
|
|
}
|
|
}
|