Implement default calls port

Signed-off-by: denis0001-dev <denis0001.dev@ya.ru>
This commit is contained in:
2026-04-29 14:00:05 +03:00
Unverified
parent 6d9066b0a6
commit 1c7dc9a8f7
5 changed files with 72 additions and 54 deletions
@@ -129,9 +129,9 @@
<string name="server_ip_label">IP или имя сервера</string>
<string name="server_ip_hint">192.168.1.10</string>
<string name="api_port_label">Порт</string>
<string name="api_port_hint">8301</string>
<string name="api_port_hint"></string>
<string name="calls_port_label">Порт звонков</string>
<string name="calls_port_hint">8302</string>
<string name="calls_port_hint"></string>
<string name="https_enabled">Защищённое соединение</string>
<string name="server_config_https_hint">HTTPS. Отключайте только для HTTP без шифрования.</string>
<string name="server_config_https_headline">HTTPS</string>
@@ -156,9 +156,9 @@
<string name="server_ip_label">Server IP or hostname</string>
<string name="server_ip_hint">192.168.1.10</string>
<string name="api_port_label">Port</string>
<string name="api_port_hint">8301</string>
<string name="api_port_hint"></string>
<string name="calls_port_label">Calls port</string>
<string name="calls_port_hint">8302</string>
<string name="calls_port_hint"></string>
<string name="https_enabled">Secure connection</string>
<string name="server_config_https_hint">Uses HTTPS. Turn off only for plain HTTP.</string>
<string name="server_config_https_headline">HTTPS</string>
@@ -11,14 +11,19 @@ import kotlinx.serialization.json.Json
import ru.fromchat.api.DeviceSessionInfo
import ru.fromchat.ui.Theme
/** Default WebRTC / calls port when none is stored or the field is left empty in UI. */
const val DEFAULT_CALLS_PORT = 8303
/**
* Server configuration: [serverIp], HTTP(S) [apiPort], optional [callsPort] (WebRTC / HAProxy; null = calls disabled). First install defaults to fromchat.ru:443 with calls on 8302.
* Server configuration: [serverIp], HTTP(S) [apiPort], [callsPort] for WebRTC / HAProxy (defaults to [DEFAULT_CALLS_PORT]).
* [callsEnabled] is updated when saving server settings (probe on the calls port); defaults to true when unset.
*/
data class ServerConfigData(
val serverIp: String,
val apiPort: Int,
val callsPort: Int?,
val callsPort: Int,
val httpsEnabled: Boolean,
val callsEnabled: Boolean = true,
)
object Settings {
@@ -28,8 +33,9 @@ object Settings {
private const val SERVER_URL_KEY = "server_url"
private const val SERVER_IP_KEY = "server_ip"
private const val API_PORT_KEY = "api_port"
/** Stored as Int; values ≤ 0 mean unset (calls disabled). */
/** Stored as Int; invalid values fall back to [DEFAULT_CALLS_PORT] when read. */
private const val CALLS_PORT_KEY = "calls_port"
private const val CALLS_ENABLED_KEY = "calls_enabled"
private const val HTTPS_ENABLED_KEY = "https_enabled"
private const val DEVICE_SESSIONS_CACHE_KEY = "device_sessions_cache_v1"
private const val LAST_SERVER_INSTANCE_ID_KEY = "last_server_instance_id"
@@ -105,7 +111,7 @@ object Settings {
if (!settings.contains(SERVER_IP_KEY)) {
settings.putString(SERVER_IP_KEY, "fromchat.ru")
settings.putInt(API_PORT_KEY, 443)
settings.putInt(CALLS_PORT_KEY, 8302)
settings.putInt(CALLS_PORT_KEY, DEFAULT_CALLS_PORT)
if (!settings.contains(HTTPS_ENABLED_KEY)) {
settings.putBoolean(HTTPS_ENABLED_KEY, true)
}
@@ -116,21 +122,28 @@ object Settings {
}
val apiPort = settings.getInt(API_PORT_KEY, 443).coerceIn(1, 65535)
val rawCalls = settings.getInt(CALLS_PORT_KEY, -1)
val calls = if (rawCalls > 0 && rawCalls <= 65535) rawCalls else null
val callsPort = if (rawCalls in 1..65535) rawCalls else DEFAULT_CALLS_PORT
val https = settings.getBoolean(HTTPS_ENABLED_KEY, true)
val callsEnabled =
if (settings.contains(CALLS_ENABLED_KEY)) {
settings.getBoolean(CALLS_ENABLED_KEY, true)
} else {
true
}
ServerConfigData(
serverIp = ip,
apiPort = apiPort,
callsPort = calls,
callsPort = callsPort,
httpsEnabled = https,
callsEnabled = callsEnabled,
)
}
set(value) = runIO {
settings.putString(SERVER_IP_KEY, value.serverIp.trim())
settings.putInt(API_PORT_KEY, value.apiPort.coerceIn(1, 65535))
val callsStored = value.callsPort?.coerceIn(1, 65535) ?: -1
settings.putInt(CALLS_PORT_KEY, callsStored)
settings.putInt(CALLS_PORT_KEY, value.callsPort.coerceIn(1, 65535))
settings.putBoolean(HTTPS_ENABLED_KEY, value.httpsEnabled)
settings.putBoolean(CALLS_ENABLED_KEY, value.callsEnabled)
}
/** Cached device sessions (JSON). Shown immediately while refreshing from the network. */
@@ -35,9 +35,9 @@ object Config {
_serverConfig.value = config
}
/** Voice/video calls (LiveKit) are enabled only when a calls (WebRTC) port is configured. */
/** Voice/video calls when the last server-config apply probe to the calls port succeeded. */
val callsEnabled: Boolean
get() = config.callsPort != null
get() = config.callsEnabled
/**
* LiveKit signaling WebSocket URL: same host and API port as HTTPS reverse proxy (`/api/livekit/rtc`).
@@ -107,6 +107,7 @@ import dev.chrisbanes.haze.rememberHazeState
import kotlin.time.TimeSource
import kotlinx.coroutines.delay
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withTimeout
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.jetbrains.compose.resources.getString
@@ -114,13 +115,12 @@ import org.jetbrains.compose.resources.stringResource
import ru.fromchat.Res
import ru.fromchat.api.ApiClient
import ru.fromchat.api.WebSocketManager
import ru.fromchat.api_port_hint
import ru.fromchat.api_port_label
import ru.fromchat.back
import ru.fromchat.calls_port_hint
import ru.fromchat.calls_port_label
import ru.fromchat.cancel
import ru.fromchat.confirm
import ru.fromchat.core.DEFAULT_CALLS_PORT
import ru.fromchat.core.ServerConfigData
import ru.fromchat.core.Settings
import ru.fromchat.core.config.Config
@@ -137,7 +137,6 @@ import ru.fromchat.server_config_snackbar_api_fail
import ru.fromchat.server_config_snackbar_defaults
import ru.fromchat.server_config_snackbar_ok_api_calls_bad
import ru.fromchat.server_config_snackbar_ok_calls
import ru.fromchat.server_config_snackbar_ok_calls_skip
import ru.fromchat.server_config_subtitle
import ru.fromchat.server_config_title
import ru.fromchat.server_ip_hint
@@ -206,6 +205,13 @@ private fun apiBaseUrlFor(config: ServerConfigData): String {
return "$scheme://${config.serverIp}:${config.apiPort}/api"
}
private suspend fun probeCallsReachable(config: ServerConfigData): Boolean {
val urlScheme = if (config.httpsEnabled) "https" else "http"
val host = config.serverIp.trim()
val root = "$urlScheme://${hostForAuthority(host)}:${config.callsPort}/"
return ApiClient.probeHttpGet(root)
}
private fun resolvedApiPort(apiPortText: String): Int {
val t = apiPortText.trim()
if (t.isEmpty()) return 443
@@ -213,9 +219,12 @@ private fun resolvedApiPort(apiPortText: String): Int {
return n.takeIf { it in 1..65535 } ?: 443
}
private fun resolvedCallsPort(callsPortText: String): Int? =
if (callsPortText.isBlank()) null
else callsPortText.trim().toIntOrNull()?.takeIf { it in 1..65535 }
/** Blank calls field uses [DEFAULT_CALLS_PORT] instead of disabling calls. */
private fun effectiveCallsPort(callsPortText: String): Int {
val t = callsPortText.trim()
if (t.isBlank()) return DEFAULT_CALLS_PORT
return t.toIntOrNull()?.takeIf { it in 1..65535 } ?: DEFAULT_CALLS_PORT
}
private suspend fun LazyListState.scrollFocusedItemIntoView(
itemIndex: Int,
@@ -421,7 +430,7 @@ fun ServerConfigScreen() {
val c = Settings.serverConfig
serverIp = c.serverIp
apiPortText = c.apiPort.toString()
callsPortText = c.callsPort?.toString().orEmpty()
callsPortText = c.callsPort.toString()
httpsEnabled = c.httpsEnabled
}
@@ -447,7 +456,7 @@ fun ServerConfigScreen() {
val callsPortError = callsPortText.isNotEmpty() && !isValidPortNumber(callsPortText)
val apiPortEffective = resolvedApiPort(apiPortText)
val callsPortParsed = resolvedCallsPort(callsPortText)
val callsPortParsed = effectiveCallsPort(callsPortText)
val canApply =
hostOk &&
!apiPortError &&
@@ -457,7 +466,7 @@ fun ServerConfigScreen() {
val resetToDefaults = {
serverIp = "fromchat.ru"
apiPortText = "443"
callsPortText = "8302"
callsPortText = DEFAULT_CALLS_PORT.toString()
httpsEnabled = true
scope.launch {
snackbarHostState.showSnackbar(strSnackbarDefaults)
@@ -466,6 +475,8 @@ fun ServerConfigScreen() {
val verifyServer: () -> Unit = {
scope.launch {
// Do not block actual checks on snackbar dismissal.
launch { snackbarHostState.showSnackbar("Checking...") }
val host = serverIp.trim()
if (host.isEmpty() || !isValidIpOrHostname(host)) {
snackbarHostState.showSnackbar(strHostError)
@@ -480,7 +491,7 @@ fun ServerConfigScreen() {
return@launch
}
val apiPort = resolvedApiPort(apiPortText)
val calls = resolvedCallsPort(callsPortText)
val calls = effectiveCallsPort(callsPortText)
val tentative = ServerConfigData(
serverIp = host,
apiPort = apiPort,
@@ -488,32 +499,24 @@ fun ServerConfigScreen() {
httpsEnabled = httpsEnabled,
)
val apiBase = apiBaseUrlFor(tentative)
val msg = runCatching {
withTimeout(3000) {
val pingMark = TimeSource.Monotonic.markNow()
val id = runCatching { ApiClient.fetchServerInstanceId(apiBase) }
.getOrNull()
?.trim()
.orEmpty()
val id = ApiClient.fetchServerInstanceId(apiBase)
val pingMs = pingMark.elapsedNow().inWholeMilliseconds
.toInt()
.coerceAtLeast(0)
if (id.isEmpty()) {
snackbarHostState.showSnackbar(strSnackbarApiFail)
return@launch
}
if (id.isEmpty()) return@withTimeout strSnackbarApiFail
val urlScheme = if (httpsEnabled) "https" else "http"
val callsOk = if (calls != null) {
val root = "$urlScheme://${hostForAuthority(host)}:${calls}/"
ApiClient.probeHttpGet(root)
} else {
null
}
val msg = when {
calls == null ->
getString(Res.string.server_config_snackbar_ok_calls_skip, pingMs)
callsOk == true ->
getString(Res.string.server_config_snackbar_ok_calls, pingMs)
else -> strSnackbarOkApiCallsBad
val callsOk = ApiClient.probeHttpGet(root)
if (callsOk) getString(Res.string.server_config_snackbar_ok_calls, pingMs)
else strSnackbarOkApiCallsBad
}
}.getOrElse { strSnackbarApiFail }
snackbarHostState.showSnackbar(msg)
}
}
@@ -621,7 +624,8 @@ fun ServerConfigScreen() {
val bearer = ApiClient.token?.trim().orEmpty()
if (bearer.isEmpty()) {
Config.updateServerConfig(tentative)
val callsOk = probeCallsReachable(tentative)
Config.updateServerConfig(tentative.copy(callsEnabled = callsOk))
Settings.lastKnownServerInstanceId = newId
WebSocketManager.disconnect()
withContext(Dispatchers.Main) {
@@ -646,7 +650,8 @@ fun ServerConfigScreen() {
return@launch
}
Config.updateServerConfig(tentative)
val callsOk = probeCallsReachable(tentative)
Config.updateServerConfig(tentative.copy(callsEnabled = callsOk))
Settings.lastKnownServerInstanceId = newId
WebSocketManager.disconnect()
WebSocketManager.connect(forceRestart = true)
@@ -839,7 +844,7 @@ fun ServerConfigScreen() {
overflow = TextOverflow.Ellipsis,
)
},
placeholder = { Text(stringResource(Res.string.api_port_hint)) },
placeholder = { Text("8301") },
modifier = Modifier
.fillMaxWidth()
.onFocusChanged {
@@ -872,7 +877,7 @@ fun ServerConfigScreen() {
overflow = TextOverflow.Ellipsis,
)
},
placeholder = { Text(stringResource(Res.string.calls_port_hint)) },
placeholder = { Text(DEFAULT_CALLS_PORT.toString()) },
modifier = Modifier
.fillMaxWidth()
.onFocusChanged {