|
|
|
|
@ -9,6 +9,8 @@ import java.nio.ByteOrder
|
|
|
|
|
const val EVT_NODEINFO_UPDATED = 16 |
|
|
|
|
const val EVT_LINK_STATUS_CHANGED = 17 |
|
|
|
|
const val EVT_LOCAL_SOCKETS = 19 |
|
|
|
|
const val EVT_CONN_LIST = 20 |
|
|
|
|
const val EVT_CONN_METRICS = 21 |
|
|
|
|
|
|
|
|
|
data class NodeStatus( |
|
|
|
|
val nodeId: Long = 0, |
|
|
|
|
@ -19,7 +21,8 @@ data class NodeStatus(
|
|
|
|
|
val bgpPresence: Boolean = false, |
|
|
|
|
val bgpUp: Boolean = false, |
|
|
|
|
val bestRtt: Int = 0, |
|
|
|
|
val rttType: Int = 0 // 0=interface, 1=nat, 2=real |
|
|
|
|
val bgpRtt: Int = 0, |
|
|
|
|
val bgpNodes: Int = 0 |
|
|
|
|
) { |
|
|
|
|
val online: Boolean get() = (directPresence && directUp) || (indirectPresence && indirectUp) |
|
|
|
|
val hasPresence: Boolean get() = directPresence || indirectPresence || bgpPresence |
|
|
|
|
@ -27,13 +30,14 @@ data class NodeStatus(
|
|
|
|
|
|
|
|
|
|
fun parseNodeStatus(data: ByteArray): NodeStatus? { |
|
|
|
|
if (data.size < 12) return null |
|
|
|
|
val buf = ByteBuffer.wrap(data, 0, 12).order(ByteOrder.LITTLE_ENDIAN) |
|
|
|
|
val buf = ByteBuffer.wrap(data, 0, data.size).order(ByteOrder.LITTLE_ENDIAN) |
|
|
|
|
val nodeId = buf.getLong() |
|
|
|
|
val presence = buf.get().toInt() and 0xFF |
|
|
|
|
val up = buf.get().toInt() and 0xFF |
|
|
|
|
val rttPacked = buf.getShort().toInt() and 0xFFFF |
|
|
|
|
val bestRtt = rttPacked and 0x3FFF |
|
|
|
|
val rttType = (rttPacked shr 14) and 3 |
|
|
|
|
val bestRtt = if (rttPacked == 0xFFFF) -1 else rttPacked |
|
|
|
|
val bgpRtt = if (data.size >= 16) buf.getShort().toInt() and 0xFFFF else 0 |
|
|
|
|
val bgpNodes = if (data.size >= 16) buf.getShort().toInt() and 0xFFFF else 0 |
|
|
|
|
return NodeStatus( |
|
|
|
|
nodeId = nodeId, |
|
|
|
|
directPresence = (presence and 1) != 0, |
|
|
|
|
@ -43,7 +47,8 @@ fun parseNodeStatus(data: ByteArray): NodeStatus? {
|
|
|
|
|
bgpPresence = (presence and 4) != 0, |
|
|
|
|
bgpUp = (up and 4) != 0, |
|
|
|
|
bestRtt = bestRtt, |
|
|
|
|
rttType = rttType |
|
|
|
|
bgpRtt = bgpRtt, |
|
|
|
|
bgpNodes = bgpNodes |
|
|
|
|
) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
@ -57,25 +62,34 @@ data class Message(val id: Long, val author: String, val text: String, val ts: L
|
|
|
|
|
|
|
|
|
|
data class ChatMember(val nodeId: Long, val name: String, val online: Boolean = false, |
|
|
|
|
val isAdmin: Boolean = false, val isSuper: Boolean = false, |
|
|
|
|
val isStorage: Boolean = false, val nodeType: Int = 0, |
|
|
|
|
val rtt: Int = 0, val rttType: Int = 0, val isSelf: Boolean = false, |
|
|
|
|
val isModer: Boolean = false, val isStorage: Boolean = false, |
|
|
|
|
val nodeType: Int = 0, |
|
|
|
|
val rtt: Int = 0, val isSelf: Boolean = false, |
|
|
|
|
val directUp: Boolean = false, val indirectUp: Boolean = false, |
|
|
|
|
val directPresence: Boolean = false, val indirectPresence: Boolean = false, |
|
|
|
|
val bgpPresence: Boolean = false) |
|
|
|
|
val bgpPresence: Boolean = false, val bgpRtt: Int = 0, |
|
|
|
|
val bgpNodes: Int = 0) { |
|
|
|
|
val flags: Int get() = (if (isSuper) 1 else 0) or (if (isAdmin) 2 else 0) or (if (isModer) 4 else 0) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
data class MemberDetail(val nodeId: Long, val name: String = "", |
|
|
|
|
val online: Boolean = false, val lastSeen: Long = 0, |
|
|
|
|
val created: Long = 0, val addresses: List<MemberAddr> = emptyList(), |
|
|
|
|
val bestRtt: Int = 0, val rttType: Int = 0, |
|
|
|
|
val bestRtt: Int = 0, |
|
|
|
|
val directPresence: Boolean = false, val directUp: Boolean = false, |
|
|
|
|
val indirectPresence: Boolean = false, val indirectUp: Boolean = false, |
|
|
|
|
val bgpPresence: Boolean = false) |
|
|
|
|
val bgpPresence: Boolean = false, val bgpRtt: Int = 0, |
|
|
|
|
val bgpNodes: Int = 0) |
|
|
|
|
|
|
|
|
|
data class MemberAddr(val family: Int = 0, val protocol: String = "", val address: String = "", |
|
|
|
|
val port: Int = 0, val socketId: Int = 0, val rtt: Int = 0) |
|
|
|
|
|
|
|
|
|
data class LinkStatus(val family: Int = 0, val address: String = "", val port: Int = 0, |
|
|
|
|
val linkState: Int = 0, val linkStatus: Int = 0) |
|
|
|
|
data class LinkStatus(val linkId: Int = 0, val localFamily: Int = 0, val localAddr: String = "", val localPort: Int = 0, |
|
|
|
|
val remoteFamily: Int = 0, val remoteAddr: String = "", val remotePort: Int = 0, |
|
|
|
|
val linkState: Int = 0, val linkStatus: Int = 0, |
|
|
|
|
val isServer: Boolean = false, val socketName: String = "") { |
|
|
|
|
val stateLabel: String get() = when (linkState) { 2 -> "UP"; 1 -> "CONNECTING"; else -> "DOWN" } |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
data class MemberLinks(val connected: Boolean = false, val initialized: Boolean = false, |
|
|
|
|
val linksUp: Int = 0, val links: List<LinkStatus> = emptyList()) |
|
|
|
|
@ -83,11 +97,128 @@ data class MemberLinks(val connected: Boolean = false, val initialized: Boolean
|
|
|
|
|
data class LocalSocketState( |
|
|
|
|
val sockId: Int = 0, val ifName: String = "", val bindIp: String = "", |
|
|
|
|
val port: Int = 0, val protocol: String = "", val state: Int = 0, |
|
|
|
|
val linksCount: Int = 0 |
|
|
|
|
val linksUp: Int = 0, val linksDown: Int = 0 |
|
|
|
|
) { |
|
|
|
|
val stateLabel: String get() = when (state) { 2 -> "UP"; 1 -> "CONNECTING"; else -> "DOWN" } |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
data class ConnInfo( |
|
|
|
|
val nodeId: Long, val name: String, val linksUp: Boolean = false, |
|
|
|
|
val initialized: Boolean = false, val proxyClient: Boolean = false, |
|
|
|
|
val proxyServer: Boolean = false, val linkCount: Int = 0, val rtt: Int = 0 |
|
|
|
|
) { |
|
|
|
|
val status: String get() = if (linksUp) "UP" else if (initialized) "init" else "DOWN" |
|
|
|
|
val rttDisplay: String get() = if (rtt > 0) String.format("%.1f ms", rtt / 10f) else "-" |
|
|
|
|
val proxyRole: String get() = when { proxyClient && proxyServer -> "CS"; proxyClient -> "C"; proxyServer -> "S"; else -> "" } |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/* Format: [count:2][entry:46B]* */ |
|
|
|
|
fun parseConnList(data: ByteArray): List<ConnInfo> { |
|
|
|
|
if (data.size < 2) return emptyList() |
|
|
|
|
val buf = ByteBuffer.wrap(data).order(ByteOrder.LITTLE_ENDIAN) |
|
|
|
|
val count = buf.getShort().toInt() and 0xFFFF |
|
|
|
|
val list = mutableListOf<ConnInfo>() |
|
|
|
|
for (i in 0 until count) { |
|
|
|
|
if (buf.position() + 46 > data.size) break |
|
|
|
|
val nodeId = buf.getLong() |
|
|
|
|
val flags = buf.get().toInt() and 0xFF |
|
|
|
|
val linkCount = buf.get().toInt() and 0xFF |
|
|
|
|
val rtt = buf.getShort().toInt() and 0xFFFF |
|
|
|
|
buf.getShort() |
|
|
|
|
val nameBytes = ByteArray(32); buf.get(nameBytes) |
|
|
|
|
val name = String(nameBytes).trimEnd('\u0000').trimEnd() |
|
|
|
|
if (nodeId == 0L) continue |
|
|
|
|
list.add(ConnInfo( |
|
|
|
|
nodeId = nodeId, name = name.ifEmpty { "%016X".format(nodeId) }, |
|
|
|
|
linksUp = (flags and 1) != 0, initialized = (flags and 2) != 0, |
|
|
|
|
proxyClient = (flags and 4) != 0, proxyServer = (flags and 8) != 0, |
|
|
|
|
linkCount = linkCount, rtt = rtt |
|
|
|
|
)) |
|
|
|
|
} |
|
|
|
|
return list |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/* ── Member wire format (79 bytes, from chat_member.h) ── */ |
|
|
|
|
data class MemberWire( |
|
|
|
|
val nodeId: Long, val online: Int, val isSelf: Boolean, |
|
|
|
|
val flags: Int, val rtt: Int, val natType: Int, |
|
|
|
|
val storage: Boolean, val name: String |
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
fun parseMemberWire(data: ByteArray, offset: Int = 0): MemberWire? { |
|
|
|
|
if (offset + 79 > data.size) return null |
|
|
|
|
val buf = ByteBuffer.wrap(data, offset, 79).order(ByteOrder.LITTLE_ENDIAN) |
|
|
|
|
val nodeId = buf.getLong() |
|
|
|
|
val online = buf.get().toInt() and 0xFF |
|
|
|
|
val isSelf = buf.get().toInt() != 0 |
|
|
|
|
val flags = buf.get().toInt() and 0xFF |
|
|
|
|
val rtt = buf.getShort().toInt() and 0xFFFF |
|
|
|
|
val natType = buf.get().toInt() and 0xFF |
|
|
|
|
val storage = buf.get().toInt() != 0 |
|
|
|
|
val nameBytes = ByteArray(64); buf.get(nameBytes) |
|
|
|
|
val name = String(nameBytes).trimEnd('\u0000') |
|
|
|
|
return MemberWire(nodeId, online, isSelf, flags, rtt, natType, storage, name) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
fun MemberWire.toChatMember(): ChatMember = ChatMember( |
|
|
|
|
nodeId = nodeId, name = name, online = online != 0, |
|
|
|
|
isAdmin = (flags and 2) != 0, isSuper = (flags and 1) != 0, |
|
|
|
|
isModer = (flags and 4) != 0, isStorage = storage, |
|
|
|
|
rtt = if (rtt == 0xFFFF) 0 else rtt, isSelf = isSelf |
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
fun cleanSocketName(raw: String): String = |
|
|
|
|
raw.removePrefix("as_").replace(Regex("_v[46]_(tcp|udp)$"), " $1") |
|
|
|
|
|
|
|
|
|
/* ── MEMBER_DETAIL (event 22) link format: 43 bytes per link ── */ |
|
|
|
|
private const val MEMBER_DETAIL_LINK_SIZE = 43 |
|
|
|
|
|
|
|
|
|
private fun addrFromBytes(family: Int, bytes: ByteArray): String { |
|
|
|
|
if (family == 4 && bytes.size >= 4) |
|
|
|
|
return "${bytes[0].toInt() and 0xFF}.${bytes[1].toInt() and 0xFF}.${bytes[2].toInt() and 0xFF}.${bytes[3].toInt() and 0xFF}" |
|
|
|
|
if (family == 6 && bytes.size >= 16) |
|
|
|
|
return bytes.take(16).joinToString(":") { "%02x".format(it) } |
|
|
|
|
return "?" |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
fun parseMemberDetail(data: ByteArray): MemberLinks? { |
|
|
|
|
if (data.size < 12) return null |
|
|
|
|
val buf = ByteBuffer.wrap(data).order(ByteOrder.LITTLE_ENDIAN) |
|
|
|
|
buf.getLong() |
|
|
|
|
val flags = buf.get().toInt() and 0xFF |
|
|
|
|
buf.getShort() |
|
|
|
|
val linkCount = buf.get().toInt() and 0xFF |
|
|
|
|
|
|
|
|
|
val links = mutableListOf<LinkStatus>() |
|
|
|
|
var offset = 12 |
|
|
|
|
for (i in 0 until linkCount) { |
|
|
|
|
if (offset + MEMBER_DETAIL_LINK_SIZE > data.size) break |
|
|
|
|
val lbuf = ByteBuffer.wrap(data, offset, MEMBER_DETAIL_LINK_SIZE).order(ByteOrder.LITTLE_ENDIAN) |
|
|
|
|
val isTcp = lbuf.get().toInt() and 0xFF |
|
|
|
|
val state = lbuf.get().toInt() and 0xFF |
|
|
|
|
val status = lbuf.get().toInt() and 0xFF |
|
|
|
|
val tcpReady = lbuf.get().toInt() and 0xFF |
|
|
|
|
val isServer = lbuf.get().toInt() and 0xFF |
|
|
|
|
val lf = lbuf.get().toInt() and 0xFF; val la = ByteArray(16); lbuf.get(la) |
|
|
|
|
val lp = lbuf.getShort().toInt() and 0xFFFF |
|
|
|
|
val rf = lbuf.get().toInt() and 0xFF; val ra = ByteArray(16); lbuf.get(ra) |
|
|
|
|
val rp = lbuf.getShort().toInt() and 0xFFFF |
|
|
|
|
val effState = if (isTcp != 0 && tcpReady != 0) 3 else state |
|
|
|
|
val effStatus = if (isTcp != 0 && tcpReady != 0) 1 else status |
|
|
|
|
links.add(LinkStatus( |
|
|
|
|
localFamily = lf, localAddr = addrFromBytes(lf, la), localPort = lp, |
|
|
|
|
remoteFamily = rf, remoteAddr = addrFromBytes(rf, ra), remotePort = rp, |
|
|
|
|
linkState = effState, linkStatus = effStatus, isServer = isServer != 0 |
|
|
|
|
)) |
|
|
|
|
offset += MEMBER_DETAIL_LINK_SIZE |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
return MemberLinks( |
|
|
|
|
connected = (flags and 1) != 0, initialized = (flags and 2) != 0, |
|
|
|
|
linksUp = if ((flags and 4) != 0) 1 else 0, links = links |
|
|
|
|
) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
class ChatRepository { |
|
|
|
|
private val myNodeId: Long = NativeLib.getMyNodeId() |
|
|
|
|
|
|
|
|
|
@ -133,20 +264,18 @@ class ChatRepository {
|
|
|
|
|
var displayText = text |
|
|
|
|
var fileSize = 0L |
|
|
|
|
|
|
|
|
|
/* Parse download state from localAttrs */ |
|
|
|
|
var downloadState = "" |
|
|
|
|
val localAttrs = obj.optString("localAttrs", "") |
|
|
|
|
if (filePath.isNotEmpty()) downloadState = "downloaded" |
|
|
|
|
else if (localAttrs.contains("\"st\":\"fl\"")) downloadState = "downloaded" |
|
|
|
|
|
|
|
|
|
/* Parse base64 waveform from voice message text (format: "wf=<100 chars A-Za-z0-9+/>;dur=1234;") */ |
|
|
|
|
var waveform = emptyList<Float>() |
|
|
|
|
var voiceDurationMs = 0 |
|
|
|
|
if (contentType == "audio/opus" && text.startsWith("wf=")) { |
|
|
|
|
try { |
|
|
|
|
val wfEnd = text.indexOf(";dur=") |
|
|
|
|
if (wfEnd > 0) { |
|
|
|
|
val wfRaw = text.substring(3, wfEnd) // 100 base64 chars |
|
|
|
|
val wfRaw = text.substring(3, wfEnd) |
|
|
|
|
waveform = if (wfRaw.length >= 50) { |
|
|
|
|
wfRaw.map { c -> |
|
|
|
|
val idx = when (c) { |
|
|
|
|
@ -158,7 +287,6 @@ class ChatRepository {
|
|
|
|
|
(idx.toFloat() / 63f).coerceIn(0f, 1f) |
|
|
|
|
} |
|
|
|
|
} else { |
|
|
|
|
/* old CSV format fallback */ |
|
|
|
|
val parts = wfRaw.split(",") |
|
|
|
|
if (parts.size >= 2) { |
|
|
|
|
parts.map { s -> |
|
|
|
|
@ -174,7 +302,6 @@ class ChatRepository {
|
|
|
|
|
} catch (_: Exception) {} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/* Parse file attachment: "filename.ext|file_size|block_size|num_blocks|sigs" */ |
|
|
|
|
if (contentType == "application/octet-stream") { |
|
|
|
|
try { |
|
|
|
|
val pipeIdx = text.indexOf('|') |
|
|
|
|
@ -226,6 +353,7 @@ class ChatRepository {
|
|
|
|
|
online = obj.optInt("on", 0) != 0, |
|
|
|
|
isAdmin = obj.optInt("ad", 0) != 0, |
|
|
|
|
isSuper = obj.optInt("sp", 0) != 0, |
|
|
|
|
isModer = obj.optInt("md", 0) != 0, |
|
|
|
|
isStorage = obj.optInt("st", 0) != 0, |
|
|
|
|
nodeType = obj.optInt("tp", 0), |
|
|
|
|
rtt = obj.optInt("rt", 0), |
|
|
|
|
@ -270,8 +398,8 @@ class ChatRepository {
|
|
|
|
|
fun setMemberNick(channelId: String, nodeId: Long, nick: String) = |
|
|
|
|
NativeLib.setMemberNick(channelId, nodeId, nick) |
|
|
|
|
|
|
|
|
|
fun setMemberFlags(channelId: String, nodeId: Long, admin: Boolean, supernode: Boolean, storage: Boolean) = |
|
|
|
|
NativeLib.setMemberFlags(channelId, nodeId, admin, supernode, storage) |
|
|
|
|
fun setMemberFlags(channelId: String, nodeId: Long, admin: Boolean, supernode: Boolean, moder: Boolean, storage: Boolean) = |
|
|
|
|
NativeLib.setMemberFlags(channelId, nodeId, admin, supernode, moder, storage) |
|
|
|
|
|
|
|
|
|
fun getMemberLinks(nodeId: Long): MemberLinks { |
|
|
|
|
try { |
|
|
|
|
@ -283,11 +411,17 @@ class ChatRepository {
|
|
|
|
|
for (i in 0 until arr.length()) { |
|
|
|
|
val a = arr.getJSONObject(i) |
|
|
|
|
links.add(LinkStatus( |
|
|
|
|
family = a.optInt("fm", 0), |
|
|
|
|
address = a.optString("ad", ""), |
|
|
|
|
port = a.optInt("po", 0), |
|
|
|
|
linkId = a.optInt("id", 0), |
|
|
|
|
localFamily = a.optInt("lf", 0), |
|
|
|
|
localAddr = a.optString("la", ""), |
|
|
|
|
localPort = a.optInt("lp", 0), |
|
|
|
|
remoteFamily = a.optInt("rf", 0), |
|
|
|
|
remoteAddr = a.optString("ra", ""), |
|
|
|
|
remotePort = a.optInt("rp", 0), |
|
|
|
|
linkState = a.optInt("st", 0), |
|
|
|
|
linkStatus = a.optInt("ls", 0) |
|
|
|
|
linkStatus = a.optInt("ls", 0), |
|
|
|
|
isServer = a.optInt("is", 0) != 0, |
|
|
|
|
socketName = a.optString("sn", "") |
|
|
|
|
)) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
@ -307,16 +441,20 @@ class ChatRepository {
|
|
|
|
|
val list = mutableListOf<LocalSocketState>() |
|
|
|
|
for (i in 0 until arr.length()) { |
|
|
|
|
val obj = arr.getJSONObject(i) |
|
|
|
|
val socketName = obj.optString("nm", "") |
|
|
|
|
val cleanName = socketName.removePrefix("as_").replace(Regex("_v[46]_(tcp|udp)$"), "") |
|
|
|
|
list.add(LocalSocketState( |
|
|
|
|
sockId = obj.optInt("si", 0), |
|
|
|
|
ifName = obj.optString("nm", ""), |
|
|
|
|
ifName = cleanName, |
|
|
|
|
bindIp = obj.optString("ad", ""), |
|
|
|
|
port = obj.optInt("po", 0), |
|
|
|
|
protocol = obj.optString("pt", ""), |
|
|
|
|
state = obj.optInt("st", 0), |
|
|
|
|
linksCount = obj.optInt("lk", 0) |
|
|
|
|
linksUp = obj.optInt("lu", 0), |
|
|
|
|
linksDown = obj.optInt("ld", 0) |
|
|
|
|
)) |
|
|
|
|
} |
|
|
|
|
list.sortWith(compareBy<LocalSocketState> { it.ifName }.thenBy { it.bindIp }.thenBy { it.protocol }.thenBy { it.port }) |
|
|
|
|
return list |
|
|
|
|
} catch (e: Exception) { Log.w("utun-gui", "getLocalSockets error", e) } |
|
|
|
|
return emptyList() |
|
|
|
|
|