package com.jttools.datacontrol.remote import org.json.JSONArray import org.json.JSONObject import java.io.IOException import java.net.HttpURLConnection import java.net.URL /** Erreur renvoyée par le serveur ([code] HTTP) ou réseau ([code] = 0), avec un message lisible. */ class ApiException(val code: Int, message: String) : IOException(message) /** * Client de l'API familyd. Appels bloquants : à lancer sur Dispatchers.IO. * Les routes des appareils passent par le tunnel ([FamilyLink.apiBase], http://10.78.0.1:8100, * déjà chiffré par WireGuard) ; l'appairage passe par l'adresse publique en HTTPS. */ class FamilyApi(private val apiBase: String, private val token: String) { fun policy(): JSONObject = JSONObject(request("GET", "$apiBase/api/device/policy", token, null)) fun serverTime(): Long = JSONObject(request("GET", "$apiBase/api/device/time", token, null)).getLong("time") fun state(state: JSONObject) { request("POST", "$apiBase/api/device/state", token, state) } fun usage(date: String, totalMs: Long, apps: Map) { request("POST", "$apiBase/api/device/usage", token, JSONObject().put("date", date).put("total_ms", totalMs).put("apps", JSONObject(apps as Map<*, *>))) } fun event(type: String, detail: String, at: Long) { request("POST", "$apiBase/api/device/events", token, JSONObject().put("type", type).put("detail", detail.take(500)).put("at", at)) } fun apps(apps: JSONArray) { request("POST", "$apiBase/api/device/apps", token, JSONObject().put("apps", apps)) } companion object { private const val CONNECT_TIMEOUT_MS = 10_000 private const val READ_TIMEOUT_MS = 15_000 /** Appairage (public, HTTPS) : renvoie la réponse brute, à lire avec [FamilyLink.fromPairResponse]. */ fun pair(serverUrl: String, code: String, publicKey: String, name: String, model: String, appVersion: String): String { require(serverUrl.startsWith("https://")) { "L'adresse du serveur doit commencer par https://" } val body = JSONObject().put("code", code).put("public_key", publicKey).put("name", name) .put("model", model).put("app_version", appVersion) return request("POST", "${serverUrl.trimEnd('/')}/api/pair", null, body) } fun request(method: String, url: String, token: String?, body: JSONObject?): String { val conn = try { (URL(url).openConnection() as HttpURLConnection).apply { requestMethod = method connectTimeout = CONNECT_TIMEOUT_MS readTimeout = READ_TIMEOUT_MS setRequestProperty("Accept", "application/json") token?.let { setRequestProperty("Authorization", "Bearer $it") } if (body != null) { doOutput = true setRequestProperty("Content-Type", "application/json; charset=utf-8") } } } catch (e: IOException) { throw ApiException(0, "Serveur injoignable : ${e.message}") } try { if (body != null) conn.outputStream.use { it.write(body.toString().toByteArray(Charsets.UTF_8)) } val code = conn.responseCode val text = (if (code in 200..299) conn.inputStream else conn.errorStream) ?.bufferedReader()?.use { it.readText() }.orEmpty() if (code !in 200..299) { val detail = runCatching { JSONObject(text).optString("detail") }.getOrNull() throw ApiException(code, detail?.takeIf { it.isNotBlank() } ?: "Erreur du serveur ($code)") } return text } catch (e: ApiException) { throw e } catch (e: IOException) { throw ApiException(0, "Serveur injoignable : ${e.message}") } finally { conn.disconnect() } } } }