package data import org.json.JSONArray import org.json.JSONObject import java.io.File /** * Desktop equivalent of the Android EncryptedSharedPreferences-backed repository. * Persists the server list — including each server's "on join perform" commands * (NickServ/ChanServ identify, etc.) — as a single JSON file (rootDir/servers.json). * Loaded once at app start and replayed automatically on every connect, so you only * ever type your identify commands in once. * * NOTE ON SECRECY: the Android version encrypts this at rest via EncryptedSharedPreferences * backed by an Android Keystore master key, so identify passwords never sit in plaintext. * There is no OS-backed secret store used here — desktop has no direct equivalent that * this project depends on. This file is plaintext JSON; the best this does is restrict * its OS file permissions to the current user (best-effort via File.setReadable/setWritable, * since Windows has no POSIX permission bits to set directly). That is meaningfully weaker * than the Android Keystore-backed encryption - treat it accordingly. */ class ServerRepository(rootDir: File) { private val file = File(rootDir, "servers.json") fun loadAll(): List { if (!file.exists()) return emptyList() return try { val arr = JSONArray(file.readText()) (0 until arr.length()).map { i -> fromJson(arr.getJSONObject(i)) } } catch (e: Exception) { emptyList() } } fun saveAll(servers: List) { val arr = JSONArray() servers.forEach { arr.put(toJson(it)) } file.parentFile?.mkdirs() file.writeText(arr.toString()) restrictToOwner(file) } fun upsert(server: ServerConfig) { val current = loadAll().toMutableList() val idx = current.indexOfFirst { it.id == server.id } if (idx >= 0) current[idx] = server else current.add(server) saveAll(current) } fun remove(serverId: String) { saveAll(loadAll().filterNot { it.id == serverId }) } // Best-effort "only the current user can read this" - Windows has no POSIX mode // bits, so File.setReadable/setWritable(_, ownerOnly = true) is the closest plain // java.io.File gets; it maps to denying the "Everyone" ACL entry rather than a // real per-user ACL, which is weaker than the Android Keystore-backed version. private fun restrictToOwner(file: File) { file.setReadable(false, false) file.setReadable(true, true) file.setWritable(false, false) file.setWritable(true, true) } private fun toJson(s: ServerConfig): JSONObject = JSONObject().apply { put("id", s.id) put("label", s.label) put("host", s.host) put("port", s.port) put("useTls", s.useTls) put("nick", s.nick) put("autoJoinChannels", JSONArray(s.autoJoinChannels)) put("saslUser", s.saslUser ?: JSONObject.NULL) put("saslPass", s.saslPass ?: JSONObject.NULL) put("onConnectCommands", JSONArray(s.onConnectCommands)) } private fun fromJson(o: JSONObject): ServerConfig { fun strArray(key: String): List { val arr = o.optJSONArray(key) ?: return emptyList() return (0 until arr.length()).map { arr.getString(it) } } return ServerConfig( id = o.getString("id"), label = o.getString("label"), host = o.getString("host"), port = o.optInt("port", 6667), useTls = o.optBoolean("useTls", false), nick = o.getString("nick"), autoJoinChannels = strArray("autoJoinChannels"), saslUser = o.optString("saslUser", null).takeIf { it != "null" }, saslPass = o.optString("saslPass", null).takeIf { it != "null" }, onConnectCommands = strArray("onConnectCommands") ) } }