From f2d6561509353fe63ad7a87e535fd40ad37bfb37 Mon Sep 17 00:00:00 2001 From: evgeny Date: Wed, 28 Oct 2026 11:05:23 +0100 Subject: [PATCH] Route Android server links through channel selection and ordinary JOIN request --- .../java/com/utun/chat/data/InviteLink.kt | 12 ++++++--- .../main/java/com/utun/chat/data/NativeLib.kt | 2 ++ .../com/utun/chat/viewmodel/ChatViewModel.kt | 26 ++++++++++++++----- 3 files changed, 29 insertions(+), 11 deletions(-) diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/InviteLink.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/InviteLink.kt index cbd5053f..8312cef9 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/InviteLink.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/InviteLink.kt @@ -3,7 +3,9 @@ package com.utun.chat.data /* Kotlin-представление invite: wire/base64 и допустимость полей проверяет общий C-кодек через NativeLib. * Декодирование не запускает подключение. Для join передавать исходный link: из preview ссылка не собирается. * Массивы моделей принадлежат получателю, copy() их не клонирует; после создания использовать только для чтения. */ -const val INVITE_LINK_VERSION: Int = 0x03 +const val INVITE_LINK_VERSION: Int = 0x04 +const val INVITE_KIND_CHANNEL: Int = 0 +const val INVITE_KIND_SERVER: Int = 1 const val INVITE_PUBKEY_SIZE: Int = 32 const val INVITE_SNAME_MAX: Int = 64 @@ -64,16 +66,18 @@ data class InviteData( val addrs: List, val password: String = "", val reality: InviteReality? = null, - val link: String = "" + val link: String = "", + val kind: Int = INVITE_KIND_CHANNEL ) { override fun equals(other: Any?): Boolean { if (this === other) return true if (other !is InviteData) return false - return channelId == other.channelId && joinKey == other.joinKey && pubkey.contentEquals(other.pubkey) && + return kind == other.kind && channelId == other.channelId && joinKey == other.joinKey && pubkey.contentEquals(other.pubkey) && nodeId == other.nodeId && addrs == other.addrs && password == other.password && reality == other.reality && link == other.link } override fun hashCode(): Int { var result = channelId.hashCode() + result = 31 * result + kind result = result * 31 + joinKey.hashCode() result = result * 31 + pubkey.contentHashCode() result = result * 31 + nodeId.hashCode() @@ -112,7 +116,7 @@ object InviteLink { (0 until addresses.length()).map { i -> val a = addresses.getJSONObject(i) InviteAddr(a.getInt("family"), bytes(a.getString("ip")), a.getInt("port"), a.getInt("socket"), a.getInt("proto")) - }, String(bytes(value.getString("password")), Charsets.UTF_8), reality, link) + }, String(bytes(value.getString("password")), Charsets.UTF_8), reality, link, value.getInt("kind")) } /** Диагностическая подпись адреса/сокета; IPv6 выводится hex без сжатия, строка не является endpoint для подключения. */ diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/NativeLib.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/NativeLib.kt index aeb92f30..99c39c96 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/NativeLib.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/NativeLib.kt @@ -111,6 +111,7 @@ object NativeLib { private val joinSequence = java.util.concurrent.atomic.AtomicLong(1) fun nextJoinRequest(): Long = joinSequence.getAndIncrement() fun joinChannel(link: String, request: Long): Int = nativeJoinChannel(link, request) + fun addServer(channel: Long, link: String, request: Long): Int = nativeAddServer(channel, link, request) fun decodeInvite(link: String): String? = nativeDecodeInvite(link) fun deriveNodeId(pubkey: ByteArray): Long = nativeDeriveNodeId(pubkey) fun connectChannel(channelId: String) { nativeConnectChannel(channelId) } @@ -367,6 +368,7 @@ object NativeLib { private external fun nativeConnectNode(address: String, port: Int, pubkeyHex: String) private external fun nativeJoinChannel(link: String, request: Long): Int private external fun nativeDecodeInvite(link: String): String? + private external fun nativeAddServer(channel: Long, link: String, request: Long): Int private external fun nativeDeriveNodeId(pubkey: ByteArray): Long private external fun nativeConnectChannel(channelId: String) private external fun nativeConnectMemberNode(channelId: String, nodeId: Long) diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/viewmodel/ChatViewModel.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/viewmodel/ChatViewModel.kt index 29e532cc..c0d5fa7e 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/viewmodel/ChatViewModel.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/viewmodel/ChatViewModel.kt @@ -154,9 +154,13 @@ class ChatViewModel : ViewModel() { private var nextPmInviteId = 0L data class LinkJoinState(val link: String, val channelId: Long, val nodeId: Long, - val pending: Boolean = true, val result: Int? = null, val request: Long = 0) + val pending: Boolean = true, val result: Int? = null, val request: Long = 0, + val serverAddition: Boolean = false) private val _linkJoin = MutableStateFlow(null) val linkJoin: StateFlow = _linkJoin + private val _serverLink = MutableStateFlow(null) + val serverLink: StateFlow = _serverLink + fun dismissServerLink() { _serverLink.value = null } private val _joinedChannel = MutableStateFlow(null) val joinedChannel: StateFlow = _joinedChannel @@ -1369,24 +1373,32 @@ class ChatViewModel : ViewModel() { LogManager.addLog("INFO", "INVITE", "PM group picker closed request=${state.requestId} pending=${state.pending}") } - fun openInviteLink(link: String) { + fun openInviteLink(link: String, serverChannel: Long = 0) { if (_linkJoin.value?.pending == true) return val invite = InviteLink.decode(link) if (invite == null) { LogManager.addLog("ERROR", "JOIN", "invalid message invite link") _linkJoin.value = LinkJoinState(link, 0, 0, pending = false, result = -1); return } - val existing = _channels.value.find { it.id.toULongOrNull()?.toLong() == invite.channelId } - if (existing != null && NativeLib.isChannelMember(existing.id, NativeLib.getMyNodeId())) { + val serverAddition = invite.kind == com.utun.chat.data.INVITE_KIND_SERVER + if (serverAddition && serverChannel == 0L) { _serverLink.value = link; return } + val channelId = if (serverAddition) serverChannel else invite.channelId + if (serverAddition && !NativeLib.isChannelMember(channelId.toString(), NativeLib.getMyNodeId())) { + LogManager.addLog("WARN", "JOIN", "server addition requires own channel ch=$channelId") + _linkJoin.value = LinkJoinState(link, channelId, invite.nodeId, pending = false, result = -7, serverAddition = true); return + } + val existing = _channels.value.find { it.id.toULongOrNull()?.toLong() == channelId } + if (!serverAddition && existing != null && NativeLib.isChannelMember(existing.id, NativeLib.getMyNodeId())) { _linkJoin.value = null; _joinedChannel.value = existing; return } if (repo == null || !NativeLib.isReady()) { LogManager.addLog("ERROR", "JOIN", "core unavailable for message invite") - _linkJoin.value = LinkJoinState(link, invite.channelId, invite.nodeId, pending = false, result = -7); return + _linkJoin.value = LinkJoinState(link, channelId, invite.nodeId, pending = false, result = -7, serverAddition = serverAddition); return } val request = NativeLib.nextJoinRequest() - _linkJoin.value = LinkJoinState(link, invite.channelId, invite.nodeId, request = request) - val result = repo?.joinChannel(link, request) ?: -7 + _serverLink.value = null + _linkJoin.value = LinkJoinState(link, channelId, invite.nodeId, request = request, serverAddition = serverAddition) + val result = if (serverAddition) NativeLib.addServer(channelId, link, request) else repo?.joinChannel(link, request) ?: -7 if (result != 0) _linkJoin.value = _linkJoin.value?.copy(pending = false, result = result) }