Merge branch 'feat/up' of github.com:p1gp1g/simplex-chat into p1gp1g-feat/up

This commit is contained in:
Evgeny Poberezkin committed 2025-08-20 11:36:21 +01:00
commit ca1a5f0eae
35 files changed
+917 -136

No files matched your search

@@ -148,6 +148,11 @@ dependencies {
//Camera Permission
implementation("com.google.accompanist:accompanist-permissions:0.34.0")
// Push notifications
implementation("org.unifiedpush.android:connector:3.0.10")
// Allow using Play Services if available
implementation("org.unifiedpush.android:embedded-fcm-distributor:3.0.0")
//implementation("androidx.compose.material:material-icons-extended:$compose_version")
//implementation("androidx.compose.ui:ui-util:$compose_version")
@@ -186,6 +186,13 @@
android:foregroundServiceType="mediaPlayback|microphone|camera|remoteMessaging"
/>
<service android:name=".PushService"
android:exported="false">
<intent-filter>
<action android:name="org.unifiedpush.android.connector.PUSH_EVENT"/>
</intent-filter>
</service>
<receiver
android:name=".CallService$CallActionReceiver"
android:enabled="true"
@@ -15,15 +15,30 @@ import java.util.concurrent.TimeUnit
object MessagesFetcherWorker {
private const val UNIQUE_WORK_TAG = BuildConfig.APPLICATION_ID + ".UNIQUE_MESSAGES_FETCHER"
fun scheduleWork(intervalSec: Int = 600, durationSec: Int = 60) {
val initialDelaySec = intervalSec.toLong()
Log.d(TAG, "Worker: scheduling work to run at ${Date(System.currentTimeMillis() + initialDelaySec * 1000)} for $durationSec sec")
/**
* Schedule [MessagesFetcherWork]
*
* @param initialDelayMs Delay in milliseconds before starting the jobs, usually = [intervalMs]
* @param intervalMs Interval in milliseconds between the end of a jobs and the start of a new one.
* If set to `0`, the job isn't restarted
* @param timeoutMs Timeout in milliseconds until we stop a job, if there are still new messages, a new job is started directly
* @param durationMs Duration in milliseconds to wait for new messages
*/
fun scheduleWork(
initialDelayMs: Int = MessagesFetcherWork.DEFAULT_INTERVAL_MS,
intervalMs: Int = MessagesFetcherWork.DEFAULT_INTERVAL_MS,
timeoutMs: Int = MessagesFetcherWork.DEFAULT_TIMEOUT_MS,
durationMs: Int = MessagesFetcherWork.DEFAULT_DURATION_MS
) {
Int.MAX_VALUE
Log.d(TAG, "Worker: scheduling work to run at ${Date(System.currentTimeMillis() + initialDelayMs)} for $timeoutMs ms max")
val periodicWorkRequest = OneTimeWorkRequest.Builder(MessagesFetcherWork::class.java)
.setInitialDelay(initialDelaySec, TimeUnit.SECONDS)
.setInitialDelay(initialDelayMs.toLong(), TimeUnit.MILLISECONDS)
.setInputData(
Data.Builder()
.putInt(MessagesFetcherWork.INPUT_DATA_INTERVAL, intervalSec)
.putInt(MessagesFetcherWork.INPUT_DATA_DURATION, durationSec)
.putInt(MessagesFetcherWork.INPUT_DATA_INTERVAL, intervalMs)
.putInt(MessagesFetcherWork.INPUT_DATA_TIMEOUT, timeoutMs)
.putInt(MessagesFetcherWork.INPUT_DATA_DURATION, durationMs)
.build()
)
.setConstraints(Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build())
@@ -45,25 +60,49 @@ class MessagesFetcherWork(
workerParams: WorkerParameters
): CoroutineWorker(context, workerParams) {
companion object {
/**
* Interval, in milliseconds, between the end of a job and the start of a new one
*/
const val INPUT_DATA_INTERVAL = "interval"
/**
* Timeout, in milliseconds, until we force stop the jobs, if there are new messages, a new job is started directly
*/
const val INPUT_DATA_TIMEOUT = "timeout"
/**
* Minimum duration, in milliseconds, to wait for new messages
*/
const val INPUT_DATA_DURATION = "duration"
private const val WAIT_AFTER_LAST_MESSAGE: Long = 10_000
const val DEFAULT_DURATION_MS = 10_000
const val DEFAULT_TIMEOUT_MS = 60_000
const val DEFAULT_INTERVAL_MS = 600_000
}
override suspend fun doWork(): Result {
// Skip when Simplex service is currently working
val durationMs = inputData.getInt(INPUT_DATA_DURATION, DEFAULT_DURATION_MS)
val timeoutMs = inputData.getInt(INPUT_DATA_TIMEOUT, DEFAULT_TIMEOUT_MS)
val intervalMs = inputData.getInt(INPUT_DATA_INTERVAL, DEFAULT_INTERVAL_MS)
// initialDelayMs may be = 0
// in this case, reschedule won't start a worker.
var initialDelayMs = intervalMs
if (SimplexService.getServiceState(SimplexApp.context) == SimplexService.ServiceState.STARTED) {
reschedule()
reschedule(
initialDelayMs = initialDelayMs,
intervalMs = intervalMs,
timeoutMs = timeoutMs,
durationMs = durationMs
)
return Result.success()
}
val durationSeconds = inputData.getInt(INPUT_DATA_DURATION, 60)
var shouldReschedule = true
try {
// In case of self-destruct is enabled the initialization process will not start in SimplexApp, Let's start it here
if (DatabaseUtils.ksSelfDestructPassword.get() != null && chatModel.chatDbStatus.value == null) {
initChatControllerOnStart()
}
withTimeout(durationSeconds * 1000L) {
withTimeout(timeoutMs.toLong()) {
val chatController = ChatController
SimplexService.waitDbMigrationEnds(chatController)
val chatDbStatus = chatController.chatModel.chatDbStatus.value
@@ -75,26 +114,48 @@ class MessagesFetcherWork(
}
Log.w(TAG, "Worker: starting work")
// Give some time to start receiving messages
delay(10_000)
delay(durationMs + 200L)
while (!isStopped) {
if (chatController.lastMsgReceivedTimestamp + WAIT_AFTER_LAST_MESSAGE < System.currentTimeMillis()) {
val lastMsgMs = System.currentTimeMillis() - chatController.lastMsgReceivedTimestamp
if (lastMsgMs > durationMs) {
Log.d(TAG, "Worker: work is done")
break
}
delay(5000)
delay(durationMs - lastMsgMs + 200L)
}
}
} catch (_: TimeoutCancellationException) { // When timeout happens
Log.d(TAG, "Worker: work is done (took $durationSeconds sec)")
Log.d(TAG, "Worker: Still work to do, restarting a new work (took $durationMs sec)")
// We reschedule a new work now (in 200 ms)
shouldReschedule = true
initialDelayMs = 200
} catch (_: CancellationException) { // When user opens the app while the worker is still working
Log.d(TAG, "Worker: interrupted")
} catch (e: Exception) {
Log.d(TAG, "Worker: unexpected exception: ${e.stackTraceToString()}")
}
if (shouldReschedule) reschedule()
if (shouldReschedule) {
reschedule(
initialDelayMs = initialDelayMs,
intervalMs = intervalMs,
timeoutMs = timeoutMs,
durationMs = durationMs
)
}
return Result.success()
}
private fun reschedule() = MessagesFetcherWorker.scheduleWork()
/**
* Reschedule a work if [initialDelayMs] > 0
*/
private fun reschedule(
initialDelayMs: Int,
intervalMs: Int,
timeoutMs: Int,
durationMs: Int
) = {
if (initialDelayMs > 0)
MessagesFetcherWorker.scheduleWork(initialDelayMs, intervalMs, timeoutMs, durationMs)
}
}
@@ -0,0 +1,92 @@
package chat.simplex.app
import chat.simplex.common.model.CC
import chat.simplex.common.platform.Log
import chat.simplex.common.platform.chatModel
import chat.simplex.common.platform.ntfManager
import chat.simplex.common.views.helpers.generalGetString
import chat.simplex.res.MR
import kotlinx.coroutines.*
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import org.unifiedpush.android.connector.FailedReason
import org.unifiedpush.android.connector.PushService
import org.unifiedpush.android.connector.data.PushEndpoint
import org.unifiedpush.android.connector.data.PushMessage
class PushService: PushService() {
private val json = Json {
ignoreUnknownKeys = true
}
@Serializable
data class PNMessage(
val verification: String? = null
)
private fun onVerification(code: String) {
CoroutineScope(Dispatchers.Default).launch {
chatModel.controller.sendCmd(
null,
CC.APIVerifySavedNtf(code),
log = true
)
}
}
override fun onMessage(message: PushMessage, instance: String) {
Log.d(TAG, "onMessage")
val pn: PNMessage = json.decodeFromString(String(message.content))
when {
pn.verification != null -> onVerification(pn.verification)
}
MessagesFetcherWorker.scheduleWork(200, 0)
}
override fun onNewEndpoint(endpoint: PushEndpoint, instance: String) {
Log.d(TAG, "onNewEndpoint")
endpoint.pubKeySet ?: run {
// Should not happen
Log.w(TAG, "Missing pubKeySet")
return
}
CoroutineScope(Dispatchers.Default).launch {
chatModel.controller.sendCmd(
null,
CC.APIRegisterWebPush(endpoint.url, endpoint.pubKeySet!!.auth, endpoint.pubKeySet!!.pubKey),
log = true
)
}
}
override fun onRegistrationFailed(reason: FailedReason, instance: String) {
Log.d(TAG, "onRegistrationFailed: $reason")
val title = generalGetString(MR.strings.icon_descr_instant_notifications)
val text = when (reason) {
FailedReason.NETWORK -> generalGetString(MR.strings.unifiedpush_registration_failed_network)
FailedReason.VAPID_REQUIRED, // Should not happen, VAPID will be required
FailedReason.INTERNAL_ERROR -> generalGetString(MR.strings.unifiedpush_registration_failed_unknown)
FailedReason.ACTION_REQUIRED -> generalGetString(MR.strings.unifiedpush_registration_failed_action)
}
ntfManager.showMessage(title, text)
}
override fun onUnregistered(instance: String) {
Log.d(TAG, "onUnregistered")
val title = generalGetString(MR.strings.icon_descr_instant_notifications)
val text = generalGetString(MR.strings.unifiedpush_unregistered)
ntfManager.showMessage(title, text)
CoroutineScope(Dispatchers.Default).launch {
chatModel.controller.sendCmd(
null,
CC.APIDeleteSavedNtf(),
log = true
)
}
}
companion object {
private const val TAG = "PushService"
}
}
@@ -20,6 +20,7 @@ import androidx.work.*
import chat.simplex.app.MainActivity.Companion.OLD_ANDROID_UI_FLAGS
import chat.simplex.app.model.NtfManager
import chat.simplex.app.model.NtfManager.AcceptCallAction
import chat.simplex.app.platform.PushManager
import chat.simplex.app.views.call.CallActivity
import chat.simplex.common.helpers.*
import chat.simplex.common.model.*
@@ -237,6 +238,16 @@ class SimplexApp: Application(), LifecycleEventObserver {
if (mode != NotificationsMode.PERIODIC) {
MessagesFetcherWorker.cancelAll()
}
if (mode == NotificationsMode.INSTANT) {
CoroutineScope(Dispatchers.Default).launch {
SimplexService.initUnifiedPush(this) {
// Change notifications mode only if everything is correctly setup
chatModel.controller.appPrefs.notificationsMode.set(mode)
}
}
} else {
chatModel.controller.appPrefs.notificationsMode.set(mode)
}
SimplexService.showBackgroundServiceNoticeIfNeeded(showOffAlert = false)
}
@@ -246,6 +257,7 @@ class SimplexApp: Application(), LifecycleEventObserver {
NotificationsMode.SERVICE -> CoroutineScope(Dispatchers.Default).launch { platform.androidServiceStart() }
NotificationsMode.PERIODIC -> SimplexApp.context.schedulePeriodicWakeUp()
NotificationsMode.OFF -> {}
NotificationsMode.INSTANT -> {}
}
}
@@ -259,10 +271,15 @@ class SimplexApp: Application(), LifecycleEventObserver {
// Prevents from showing "Enable notifications" alert when onboarding wasn't complete yet
if (chatModel.controller.appPrefs.onboardingStage.get() == OnboardingStage.OnboardingComplete) {
SimplexService.showBackgroundServiceNoticeIfNeeded()
if (appPrefs.notificationsMode.get() == NotificationsMode.SERVICE)
withBGApi {
when (appPrefs.notificationsMode.get()) {
NotificationsMode.SERVICE -> withBGApi {
platform.androidServiceStart()
}
NotificationsMode.INSTANT -> withBGApi {
PushManager.initStart(context)
}
else -> {}
}
}
}
@@ -370,6 +387,7 @@ class SimplexApp: Application(), LifecycleEventObserver {
override fun androidCreateActiveCallState(): Closeable = ActiveCallState()
override val androidApiLevel: Int get() = Build.VERSION.SDK_INT
override val supportsPushNotifications = true
}
}
@@ -22,6 +22,7 @@ import androidx.core.app.ServiceCompat
import androidx.core.content.ContextCompat
import androidx.work.*
import chat.simplex.app.model.NtfManager
import chat.simplex.app.platform.PushManager
import chat.simplex.common.AppLock
import chat.simplex.common.helpers.requiresIgnoringBattery
import chat.simplex.common.model.ChatController
@@ -427,6 +428,10 @@ class SimplexService: Service() {
private fun getPreferences(context: Context): SharedPreferences = context.getSharedPreferences(SHARED_PREFS_ID, Context.MODE_PRIVATE)
suspend fun initUnifiedPush(scope: CoroutineScope, onSuccess: () -> Unit) {
PushManager.initUnifiedPush(androidAppContext, scope, onSuccess)
}
fun showBackgroundServiceNoticeIfNeeded(showOffAlert: Boolean = true) {
val appPrefs = ChatController.appPrefs
val mode = appPrefs.notificationsMode.get()
@@ -230,6 +230,9 @@ object NtfManager {
val builder = NotificationCompat.Builder(context, MessageChannel)
.setContentTitle(title)
.setContentText(text)
.setStyle(
NotificationCompat.BigTextStyle().bigText(text)
)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setGroup(MessageGroup)
.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_CHILDREN)
@@ -0,0 +1,341 @@
package chat.simplex.app.platform
import SectionItemView
import android.content.Context
import android.content.pm.PackageManager
import android.os.Build
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CornerSize
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.*
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import chat.simplex.common.model.*
import chat.simplex.common.model.ChatController.getUserServers
import chat.simplex.common.platform.Log
import chat.simplex.common.platform.chatModel
import chat.simplex.common.views.helpers.*
import chat.simplex.common.views.usersettings.networkAndServers.showAddServerDialog
import chat.simplex.res.MR
import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
import kotlinx.coroutines.*
import org.unifiedpush.android.connector.UnifiedPush
import androidx.core.net.toUri
/**
* Object with functions to interact with push services
*/
object PushManager {
private const val TAG = "PushManager"
private val VAPID_REGEX = Regex("^[A-Za-z0-9_-]{87}(=+)?$")
/**
* Check if a NTF server is setup and ask user's push distributor
*
* Show an alert if NTF server isn't define,
* If a single distrib is available, use it
* If many, ask distributor to use with a dialog
* Else alert about missing service
*/
suspend fun initUnifiedPush(context: Context, scope: CoroutineScope, onSuccess: () -> Unit) {
val rh = chatModel.remoteHostId()
val userServers = getUserServers(rh) ?: listOf()
val userNtfServer = userServers.userNtfServer()
?: run {
Log.d(TAG, "User doesn't have any NTF server")
showMissingNTFDialog(scope, rh, userServers)
// After coming back from the server view, users will have to click on "Instant" again
return
}
val vapid = userNtfServer.vapid()
?: run {
Log.d(TAG, "User NTF Server doesn't have any VAPID FP")
showNTFNoVAPIDDialog(scope, rh, userServers)
// After coming back from the server view, users will have to click on "Instant" again
return
}
val distributors = UnifiedPush.getDistributors(context)
when (distributors.size) {
0 -> {
Log.d(TAG, "No distributor found")
showMissingPushServiceDialog()
}
1 -> {
Log.d(TAG, "Found only one distributor installed")
UnifiedPush.saveDistributor(context, distributors.first())
register(context, vapid)
onSuccess()
}
else -> {
Log.d(TAG, "Found many distributors installed")
showSelectPushServiceIntroDialog {
showSelectPushServiceDialog(context, distributors) {
UnifiedPush.saveDistributor(context, it)
register(context, vapid)
onSuccess()
}
}
}
}
}
/**
* Register to UnifiedPush distributor if any is already used
*
* To run when the app starts; call [chat.simplex.app.PushService.onUnregistered]
* if the distributor is uninstalled
*/
suspend fun initStart(context: Context) {
Log.d(TAG, "Init UnifiedPush during app startup")
//TODO: limit to once a day to reduce registrations to ntf server ?
UnifiedPush.getAckDistributor(context)?.let {
val vapid = getUserServers(chatModel.remoteHostId())
?.userNtfServer()
?.vapid()
// Unregister if the user deleted the ntf server or change it with one without vapid
?: return UnifiedPush.unregister(context)
register(context, vapid)
}
}
private fun register(context: Context, vapid: String) {
UnifiedPush.register(context, vapid = vapid)
}
/**
* Get user NTF server
*/
private fun List<UserOperatorServers>.userNtfServer(): UserServer? {
return this.firstNotNullOfOrNull {
it.ntfServers.firstOrNull { s -> s.enabled }
}
}
/**
* Get vapid fingerprint from the URI
*/
private fun UserServer.vapid(): String ? = server
.toUri()
.getQueryParameter("vapid")
?.takeIf { VAPID_REGEX.matches(it) }
/**
* Show a dialog to inform about missing NTF server
*/
private fun showMissingNTFDialog(scope: CoroutineScope, rh: Long?, userServers: List<UserOperatorServers>) = AlertManager.shared.showAlert {
AlertDialog(
onDismissRequest = AlertManager.shared::hideAlert,
title = {
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Icon(
painterResource(MR.images.ic_warning),
contentDescription = null, // The icon doesn't add any meaning and must not be transcripted with screen readers
)
Text(
stringResource(MR.strings.icon_descr_instant_notifications),
fontWeight = FontWeight.Bold
)
}
},
text = {
Text(stringResource(MR.strings.warning_push_needs_ntf_server))
},
// Go to user's servers settings
confirmButton = {
TextButton(onClick = {
AlertManager.shared.hideAlert()
showAddServerDialog(scope, rh, userServers)
}) { Text(stringResource(MR.strings.smp_servers_add)) }
},
// Ignore
dismissButton = {
TextButton(onClick = AlertManager.shared::hideAlert) { Text(stringResource(android.R.string.cancel)) }
},
shape = RoundedCornerShape(corner = CornerSize(25.dp))
)
}
/**
* Show a dialog to inform about missing VAPID with the NTF server
*/
private fun showNTFNoVAPIDDialog(scope: CoroutineScope, rh: Long?, userServers: List<UserOperatorServers>) = AlertManager.shared.showAlert {
AlertDialog(
onDismissRequest = AlertManager.shared::hideAlert,
title = {
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Icon(
painterResource(MR.images.ic_warning),
contentDescription = null, // The icon doesn't add any meaning and must not be transcripted with screen readers
)
Text(
stringResource(MR.strings.icon_descr_instant_notifications),
fontWeight = FontWeight.Bold
)
}
},
text = {
Text(stringResource(MR.strings.warning_push_needs_ntf_server_with_vapid))
},
// Go to user's servers settings
confirmButton = {
TextButton(onClick = {
AlertManager.shared.hideAlert()
showAddServerDialog(scope, rh, userServers)
}) { Text(stringResource(MR.strings.smp_servers_add)) }
},
// Ignore
dismissButton = {
TextButton(onClick = AlertManager.shared::hideAlert) { Text(stringResource(android.R.string.cancel)) }
},
shape = RoundedCornerShape(corner = CornerSize(25.dp))
)
}
/**
* Dialog to add a server, manually or with a QR code
*/
private fun showAddServerDialog(scope: CoroutineScope, rh: Long?, userServers: List<UserOperatorServers>) {
val userServersState = mutableStateOf(userServers)
val serverErrors = mutableStateOf(listOf<UserServersError>())
showAddServerDialog(scope, userServersState, serverErrors, rh)
}
/**
* Show a dialog to inform about missing push service
*/
private fun showMissingPushServiceDialog() = AlertManager.shared.showAlert {
AlertDialog(
onDismissRequest = AlertManager.shared::hideAlert,
title = {
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Icon(
painterResource(MR.images.ic_warning),
contentDescription = null, // The icon doesn't add any meaning and must not be transcripted with screen readers
)
Text(
stringResource(MR.strings.icon_descr_instant_notifications),
fontWeight = FontWeight.Bold
)
}
},
text = {
Text(
buildAnnotatedString {
append(stringResource(MR.strings.warning_push_needs_push_service))
withLink(LinkAnnotation.Url(url = "https://unifiedpush.org")) {
withStyle(style = SpanStyle(color = MaterialTheme.colors.primary)) {
append("https://unifiedpush.org")
}
}
}
)
},
confirmButton = {
TextButton(onClick = AlertManager.shared::hideAlert) { Text(stringResource(android.R.string.ok)) }
},
shape = RoundedCornerShape(corner = CornerSize(25.dp))
)
}
/**
* Show an intro to explain why they need to select a push service
*/
private fun showSelectPushServiceIntroDialog(onConfirm: () -> Unit) = AlertManager.shared.showAlert {
AlertDialog(
onDismissRequest = AlertManager.shared::hideAlert,
title = {
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Icon(
painterResource(MR.images.ic_notifications),
contentDescription = null, // The icon doesn't add any meaning and must not be transcripted with screen readers
)
Text(
stringResource(MR.strings.icon_descr_instant_notifications),
fontWeight = FontWeight.Bold
)
}
},
text = {
Text(stringResource(MR.strings.select_push_service_intro))
},
confirmButton = {
TextButton(
onClick = {
AlertManager.shared.hideAlert()
onConfirm()
}
) { Text(stringResource(MR.strings.select_push_service)) }
},
dismissButton = {
TextButton(onClick = AlertManager.shared::hideAlert) { Text(stringResource(android.R.string.cancel)) }
},
shape = RoundedCornerShape(corner = CornerSize(25.dp))
)
}
/**
* Show a dialog to select push service
*
* @param distributors List of installed distributors' packageName
* @param onSelected run when a distributor is selected, its param is the selected distrib packageName
*/
private fun showSelectPushServiceDialog(context: Context, distributors: List<String>, onSelected: (String) -> Unit) = AlertManager.shared.showAlert {
AlertDialog(
onDismissRequest = AlertManager.shared::hideAlert,
title = {
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Icon(
painterResource(MR.images.ic_notifications),
contentDescription = null, // The icon doesn't add any meaning and must not be transcripted with screen readers
)
Text(
stringResource(MR.strings.select_push_service),
fontWeight = FontWeight.Bold
)
}
},
text = {
Column {
distributors.map {
if (it == context.packageName) it to "Play Services"
else it to context.getApplicationName(it)
}.forEach {
SectionItemView({
AlertManager.shared.hideAlert()
onSelected(it.first)
}) {
Text(it.second)
}
}
}
},
confirmButton = {
TextButton(onClick = AlertManager.shared::hideAlert) { Text(stringResource(android.R.string.cancel)) }
},
shape = RoundedCornerShape(corner = CornerSize(25.dp))
)
}
private fun Context.getApplicationName(applicationId: String): String {
return try {
val ai = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
packageManager.getApplicationInfo(
applicationId,
PackageManager.ApplicationInfoFlags.of(
PackageManager.GET_META_DATA.toLong()
)
)
} else {
packageManager.getApplicationInfo(applicationId, 0)
}
packageManager.getApplicationLabel(ai)
} catch (e: PackageManager.NameNotFoundException) {
applicationId
} as String
}
}
@@ -9,7 +9,7 @@ val NotificationsMode.requiresIgnoringBatterySinceSdk: Int get() = when(this) {
NotificationsMode.OFF -> Int.MAX_VALUE
NotificationsMode.PERIODIC -> Build.VERSION_CODES.M
NotificationsMode.SERVICE -> Build.VERSION_CODES.S
/*INSTANT -> Int.MAX_VALUE - for Firebase notifications */
NotificationsMode.INSTANT -> Int.MAX_VALUE
}
val NotificationsMode.requiresIgnoringBattery
@@ -3693,6 +3693,9 @@ sealed class CC {
class ResetAgentServersStats(): CC()
class GetAgentSubsTotal(val userId: Long): CC()
class GetAgentServersSummary(val userId: Long): CC()
class APIRegisterWebPush(val endpoint: String, val auth: String, val p256dh: String): CC()
class APIVerifySavedNtf(val code: String): CC()
class APIDeleteSavedNtf(): CC()
val cmdString: String get() = when (this) {
is Console -> cmd
@@ -3895,6 +3898,9 @@ sealed class CC {
is ResetAgentServersStats -> "/reset servers stats"
is GetAgentSubsTotal -> "/get subs total $userId"
is GetAgentServersSummary -> "/get servers summary $userId"
is APIRegisterWebPush -> "/_ntf register webpush $endpoint $auth $p256dh INSTANT"
is APIDeleteSavedNtf -> "/_ntf delete saved"
is APIVerifySavedNtf -> "/_ntf verify $code"
}
val cmdType: String get() = when (this) {
@@ -4063,6 +4069,9 @@ sealed class CC {
is ResetAgentServersStats -> "resetAgentServersStats"
is GetAgentSubsTotal -> "getAgentSubsTotal"
is GetAgentServersSummary -> "getAgentServersSummary"
is APIRegisterWebPush -> "apiRegisterWebPush"
is APIDeleteSavedNtf -> "apiDeleteSavedNtf"
is APIVerifySavedNtf -> "apiVerifySavedNtf"
}
data class ItemRange(val from: Long, val to: Long)
@@ -4163,6 +4172,7 @@ class DBEncryptionConfig(val currentKey: String, val newKey: String)
@Serializable
enum class ServerProtocol {
@SerialName("ntf") NTF,
@SerialName("smp") SMP,
@SerialName("xftp") XFTP;
}
@@ -4280,6 +4290,12 @@ data class ServerOperator(
val serverDomains: List<String>,
val conditionsAcceptance: ConditionsAcceptance,
val enabled: Boolean,
/**
* Roles for ntf servers
*
* default to (true, true), for transition from old ServerOperator
*/
val ntfRoles: ServerRoles = ServerRoles(true, true),
val smpRoles: ServerRoles,
val xftpRoles: ServerRoles,
) {
@@ -4301,6 +4317,7 @@ data class ServerOperator(
serverDomains = listOf("simplex.im"),
conditionsAcceptance = ConditionsAcceptance.Accepted(acceptedAt = null, autoAccepted = false),
enabled = true,
ntfRoles = ServerRoles(storage = true, proxy = true),
smpRoles = ServerRoles(storage = true, proxy = true),
xftpRoles = ServerRoles(storage = true, proxy = true)
)
@@ -4318,6 +4335,7 @@ data class ServerOperator(
other.serverDomains == this.serverDomains &&
other.conditionsAcceptance == this.conditionsAcceptance &&
other.enabled == this.enabled &&
other.ntfRoles == this.ntfRoles &&
other.smpRoles == this.smpRoles &&
other.xftpRoles == this.xftpRoles
}
@@ -4330,6 +4348,7 @@ data class ServerOperator(
result = 31 * result + serverDomains.hashCode()
result = 31 * result + conditionsAcceptance.hashCode()
result = 31 * result + enabled.hashCode()
result = 31 * result + ntfRoles.hashCode()
result = 31 * result + smpRoles.hashCode()
result = 31 * result + xftpRoles.hashCode()
return result
@@ -4368,6 +4387,7 @@ data class ServerRoles(
@Serializable
data class UserOperatorServers(
val operator: ServerOperator?,
val ntfServers: List<UserServer> = listOf(),
val smpServers: List<UserServer>,
val xftpServers: List<UserServer>
) {
@@ -4383,6 +4403,7 @@ data class UserOperatorServers(
serverDomains = emptyList(),
conditionsAcceptance = ConditionsAcceptance.Accepted(null, autoAccepted = false),
enabled = false,
ntfRoles = ServerRoles(storage = true, proxy = true),
smpRoles = ServerRoles(storage = true, proxy = true),
xftpRoles = ServerRoles(storage = true, proxy = true)
)
@@ -4390,12 +4411,14 @@ data class UserOperatorServers(
companion object {
val sampleData1 = UserOperatorServers(
operator = ServerOperator.sampleData1,
ntfServers = listOf(),
smpServers = listOf(UserServer.sampleData.preset),
xftpServers = listOf(UserServer.sampleData.xftpPreset)
)
val sampleDataNilOperator = UserOperatorServers(
operator = null,
ntfServers = listOf(),
smpServers = listOf(UserServer.sampleData.preset),
xftpServers = listOf(UserServer.sampleData.xftpPreset)
)
@@ -4411,6 +4434,7 @@ sealed class UserServersError {
val globalError: String?
get() = when (this.protocol_) {
ServerProtocol.NTF -> globalNTFError
ServerProtocol.SMP -> globalSMPError
ServerProtocol.XFTP -> globalXFTPError
}
@@ -4459,6 +4483,24 @@ sealed class UserServersError {
null
}
val globalNTFError: String?
get() = if (this.protocol_ == ServerProtocol.SMP) {
when (this) {
is NoServers -> this.user?.let { "${userStr(it)} ${generalGetString(MR.strings.no_notif_servers_configured)}" }
?: generalGetString(MR.strings.no_notif_servers_configured)
is StorageMissing -> this.user?.let { "${userStr(it)} ${generalGetString(MR.strings.no_notif_servers_configured_for_receiving)}" }
?: generalGetString(MR.strings.no_notif_servers_configured_for_receiving)
is ProxyMissing -> this.user?.let { "${userStr(it)} ${generalGetString(MR.strings.no_notif_servers_configured_for_receiving)}" }
?: generalGetString(MR.strings.no_notif_servers_configured_for_receiving)
else -> null
}
} else {
null
}
private fun userStr(user: UserRef): String {
return String.format(generalGetString(MR.strings.for_chat_profile), user.localDisplayName)
}
@@ -7320,7 +7362,7 @@ sealed class AgentErrorType {
is CMD -> "CMD ${cmdErr.string} $errContext"
is CONN -> "CONN ${connErr.string} $errContext"
is SMP -> "SMP ${smpErr.string}"
// is NTF -> "NTF ${ntfErr.string}"
is NTF -> "NTF ${ntfErr.string}"
is XFTP -> "XFTP ${xftpErr.string}"
is PROXY -> "PROXY $proxyServer $relayServer ${proxyErr.string}"
is RCP -> "RCP ${rcpErr.string}"
@@ -7333,7 +7375,7 @@ sealed class AgentErrorType {
@Serializable @SerialName("CMD") class CMD(val cmdErr: CommandErrorType, val errContext: String): AgentErrorType()
@Serializable @SerialName("CONN") class CONN(val connErr: ConnectionErrorType, val errContext: String): AgentErrorType()
@Serializable @SerialName("SMP") class SMP(val serverAddress: String, val smpErr: SMPErrorType): AgentErrorType()
// @Serializable @SerialName("NTF") class NTF(val ntfErr: SMPErrorType): AgentErrorType()
@Serializable @SerialName("NTF") class NTF(val ntfErr: SMPErrorType): AgentErrorType()
@Serializable @SerialName("XFTP") class XFTP(val xftpErr: XFTPErrorType): AgentErrorType()
@Serializable @SerialName("PROXY") class PROXY(val proxyServer: String, val relayServer: String, val proxyErr: ProxyClientError): AgentErrorType()
@Serializable @SerialName("RCP") class RCP(val rcpErr: RCErrorType): AgentErrorType()
@@ -7691,7 +7733,7 @@ sealed class RemoteCtrlError {
}
enum class NotificationsMode() {
OFF, PERIODIC, SERVICE, /*INSTANT - for Firebase notifications */;
OFF, PERIODIC, SERVICE, INSTANT;
companion object {
val default: NotificationsMode = SERVICE
@@ -7918,6 +7960,7 @@ enum class AppSettingsNotificationMode {
companion object {
fun from(mode: NotificationsMode): AppSettingsNotificationMode =
when (mode) {
NotificationsMode.INSTANT -> INSTANT
NotificationsMode.SERVICE -> INSTANT
NotificationsMode.PERIODIC -> PERIODIC
NotificationsMode.OFF -> OFF
@@ -33,6 +33,7 @@ interface PlatformInterface {
@Composable fun androidLockPortraitOrientation() {}
suspend fun androidAskToAllowBackgroundCalls(): Boolean = true
@Composable fun desktopShowAppUpdateNotice() {}
val supportsPushNotifications: Boolean get() = false
}
/**
* Multiplatform project has separate directories per platform + common directory that contains directories per platform + common for all of them.
@@ -133,6 +133,15 @@ private fun notificationModes(): List<ValueTitleDesc<NotificationsMode>> {
AnnotatedString(generalGetString(MR.strings.notifications_mode_service_desc)),
)
)
if (platform.supportsPushNotifications) {
res.add(
ValueTitleDesc(
NotificationsMode.INSTANT,
generalGetString(MR.strings.notifications_mode_instant),
AnnotatedString(generalGetString(MR.strings.notifications_mode_instant_desc))
)
)
}
return res
}
@@ -164,6 +173,10 @@ fun notificationPreviewModes(): List<ValueTitleDesc<NotificationPreviewMode>> {
}
fun changeNotificationsMode(mode: NotificationsMode, chatModel: ChatModel) {
chatModel.controller.appPrefs.notificationsMode.set(mode)
platform.androidNotificationsModeChanged(mode)
// the preference is updated in androidNotificationsModeChanged for Android
if (appPlatform.isAndroid) {
platform.androidNotificationsModeChanged(mode)
} else {
chatModel.controller.appPrefs.notificationsMode.set(mode)
}
}
@@ -932,6 +932,15 @@ fun globalXFTPServersError(serverErrors: List<UserServersError>): String? {
return null
}
fun globalNTFServersError(serverErrors: List<UserServersError>): String? {
for (err in serverErrors) {
if (err.globalNTFError != null) {
return err.globalNTFError
}
}
return null
}
fun findDuplicateHosts(serverErrors: List<UserServersError>): Set<String> {
val duplicateHostsList = serverErrors.mapNotNull { err ->
if (err is UserServersError.DuplicateServer) {
@@ -107,12 +107,15 @@ fun addServer(
val operatorServers = updatedUserServers[operatorIndex]
// Create a mutable copy of the smpServers or xftpServers and add the server
when (serverProtocol) {
ServerProtocol.NTF -> {
// We use a single ntf server
updatedUserServers[operatorIndex] = operatorServers.copy(ntfServers = listOf(server))
}
ServerProtocol.SMP -> {
val updatedSMPServers = operatorServers.smpServers.toMutableList()
updatedSMPServers.add(server)
updatedUserServers[operatorIndex] = operatorServers.copy(smpServers = updatedSMPServers)
}
ServerProtocol.XFTP -> {
val updatedXFTPServers = operatorServers.xftpServers.toMutableList()
updatedXFTPServers.add(server)
@@ -101,27 +101,26 @@ fun navigateToProtocolView(
userServers = userServers,
serverErrors = serverErrors,
onDelete = {
if (protocol == ServerProtocol.SMP) {
deleteSMPServer(userServers, operatorIndex, serverIndex)
} else {
deleteXFTPServer(userServers, operatorIndex, serverIndex)
when (protocol) {
ServerProtocol.NTF -> deleteNTFServer(userServers, operatorIndex, serverIndex)
ServerProtocol.SMP -> deleteSMPServer(userServers, operatorIndex, serverIndex)
ServerProtocol.XFTP -> deleteXFTPServer(userServers, operatorIndex, serverIndex)
}
close()
},
onUpdate = { updatedServer ->
userServers.value = userServers.value.toMutableList().apply {
this[operatorIndex] = this[operatorIndex].copy(
smpServers = if (protocol == ServerProtocol.SMP) {
this[operatorIndex].smpServers.toMutableList().apply {
this[serverIndex] = updatedServer
}
} else this[operatorIndex].smpServers,
xftpServers = if (protocol == ServerProtocol.XFTP) {
this[operatorIndex].xftpServers.toMutableList().apply {
this[serverIndex] = updatedServer
}
} else this[operatorIndex].xftpServers
)
if (platform.supportsPushNotifications && protocol == ServerProtocol.NTF && updatedServer.enabled) {
// We keep a single ntf server, if the updatedServer is enabled, we disable all other ntf servers first
this.replaceAll { op ->
op.copy(ntfServers = op.ntfServers.map { server -> server.copy(enabled = false).also { s -> Log.d(TAG, "ntf: $s")} })
}
}
this[operatorIndex] = when (protocol) {
ServerProtocol.NTF -> this[operatorIndex].copy(ntfServers = this[operatorIndex].ntfServers.toMutableList().apply { this[serverIndex] = updatedServer })
ServerProtocol.SMP -> this[operatorIndex].copy(smpServers = this[operatorIndex].smpServers.toMutableList().apply { this[serverIndex] = updatedServer })
ServerProtocol.XFTP -> this[operatorIndex].copy(xftpServers = this[operatorIndex].xftpServers.toMutableList().apply { this[serverIndex] = updatedServer })
}
}
},
close = close,
@@ -387,14 +386,19 @@ fun OperatorViewLayout(
testing = testing,
smpServers = userServers.value[operatorIndex].smpServers,
xftpServers = userServers.value[operatorIndex].xftpServers,
ntfServers = userServers.value[operatorIndex].ntfServers,
) { p, l ->
when (p) {
ServerProtocol.NTF -> userServers.value = userServers.value.toMutableList().apply {
this[operatorIndex] = this[operatorIndex].copy(
ntfServers = l
)
}
ServerProtocol.XFTP -> userServers.value = userServers.value.toMutableList().apply {
this[operatorIndex] = this[operatorIndex].copy(
xftpServers = l
)
}
ServerProtocol.SMP -> userServers.value = userServers.value.toMutableList().apply {
this[operatorIndex] = this[operatorIndex].copy(
smpServers = l
@@ -115,7 +115,11 @@ private fun ProtocolServerLayout(
onDelete: () -> Unit,
) {
ColumnWithScrollBar {
AppBarTitle(stringResource(if (serverProtocol == ServerProtocol.XFTP) MR.strings.xftp_server else MR.strings.smp_server))
AppBarTitle(stringResource(when (serverProtocol) {
ServerProtocol.NTF -> MR.strings.ntf_server
ServerProtocol.XFTP -> MR.strings.xftp_server
ServerProtocol.SMP -> MR.strings.smp_server
}))
if (server.value.preset) {
PresetServer(server, testing, testServer)
@@ -148,13 +148,41 @@ fun YourServersViewLayout(
}
}
if (platform.supportsPushNotifications) {
if (userServers.value[operatorIndex].ntfServers.any { !it.deleted }) {
SectionDividerSpaced()
SectionView(generalGetString(MR.strings.notif_servers).uppercase()) {
userServers.value[operatorIndex].ntfServers.forEachIndexed { i, server ->
if (server.deleted) return@forEachIndexed
SectionItemView({ navigateToProtocolView(i, server, ServerProtocol.NTF) }) {
ProtocolServerViewLink(
srv = server,
serverProtocol = ServerProtocol.NTF,
duplicateHosts = duplicateHosts
)
}
}
}
val ntfErr = globalNTFServersError(serverErrors.value)
if (ntfErr != null) {
SectionCustomFooter {
ServersErrorFooter(ntfErr)
}
} else {
SectionTextFooter(generalGetString(MR.strings.ntf_servers_per_user))
}
}
}
if (
userServers.value[operatorIndex].ntfServers.any { !it.deleted } && platform.supportsPushNotifications ||
userServers.value[operatorIndex].smpServers.any { !it.deleted } ||
userServers.value[operatorIndex].xftpServers.any { !it.deleted }
) {
SectionDividerSpaced(maxTopPadding = false, maxBottomPadding = false)
}
SectionView {
SettingsActionItem(
painterResource(MR.images.ic_add),
@@ -178,14 +206,19 @@ fun YourServersViewLayout(
testing = testing,
smpServers = userServers.value[operatorIndex].smpServers,
xftpServers = userServers.value[operatorIndex].xftpServers,
ntfServers = userServers.value[operatorIndex].ntfServers,
) { p, l ->
when (p) {
ServerProtocol.NTF -> userServers.value = userServers.value.toMutableList().apply {
this[operatorIndex] = this[operatorIndex].copy(
ntfServers = l
)
}
ServerProtocol.XFTP -> userServers.value = userServers.value.toMutableList().apply {
this[operatorIndex] = this[operatorIndex].copy(
xftpServers = l
)
}
ServerProtocol.SMP -> userServers.value = userServers.value.toMutableList().apply {
this[operatorIndex] = this[operatorIndex].copy(
smpServers = l
@@ -204,16 +237,17 @@ fun YourServersViewLayout(
fun TestServersButton(
smpServers: List<UserServer>,
xftpServers: List<UserServer>,
ntfServers: List<UserServer>,
testing: MutableState<Boolean>,
onUpdate: (ServerProtocol, List<UserServer>) -> Unit
) {
val scope = rememberCoroutineScope()
val disabled = derivedStateOf { (smpServers.none { it.enabled } && xftpServers.none { it.enabled }) || testing.value }
val disabled = derivedStateOf { (smpServers.none { it.enabled } && xftpServers.none { it.enabled } && ntfServers.none { it.enabled }) || testing.value }
SectionItemView(
{
scope.launch {
testServers(testing, smpServers, xftpServers, chatModel, onUpdate)
testServers(testing, smpServers, xftpServers, ntfServers, chatModel, onUpdate)
}
},
disabled = disabled.value
@@ -303,6 +337,7 @@ private suspend fun testServers(
testing: MutableState<Boolean>,
smpServers: List<UserServer>,
xftpServers: List<UserServer>,
ntfServers: List<UserServer>,
m: ChatModel,
onUpdate: (ServerProtocol, List<UserServer>) -> Unit
) {
@@ -310,11 +345,14 @@ private suspend fun testServers(
onUpdate(ServerProtocol.SMP, smpResetStatus)
val xftpResetStatus = resetTestStatus(xftpServers)
onUpdate(ServerProtocol.XFTP, xftpResetStatus)
val ntfResetStatus = resetTestStatus(ntfServers)
onUpdate(ServerProtocol.NTF, ntfResetStatus)
testing.value = true
val smpFailures = runServersTest(smpResetStatus, m) { onUpdate(ServerProtocol.SMP, it) }
val xftpFailures = runServersTest(xftpResetStatus, m) { onUpdate(ServerProtocol.XFTP, it) }
val ntfFailures = runServersTest(ntfResetStatus, m) { onUpdate(ServerProtocol.NTF, it) }
testing.value = false
val fs = smpFailures + xftpFailures
val fs = smpFailures + xftpFailures + ntfFailures
if (fs.isNotEmpty()) {
val msg = fs.map { it.key + ": " + it.value.localizedDescription }.joinToString("\n")
AlertManager.shared.showAlertMsg(
@@ -354,6 +392,32 @@ private suspend fun runServersTest(servers: List<UserServer>, m: ChatModel, onUp
return fs
}
fun deleteNTFServer(
userServers: MutableState<List<UserOperatorServers>>,
operatorServersIndex: Int,
serverIndex: Int
) {
val serverIsSaved = userServers.value[operatorServersIndex].ntfServers[serverIndex].serverId != null
if (serverIsSaved) {
userServers.value = userServers.value.toMutableList().apply {
this[operatorServersIndex] = this[operatorServersIndex].copy(
ntfServers = this[operatorServersIndex].ntfServers.toMutableList().apply {
this[serverIndex] = this[serverIndex].copy(deleted = true)
}
)
}
} else {
userServers.value = userServers.value.toMutableList().apply {
this[operatorServersIndex] = this[operatorServersIndex].copy(
ntfServers = this[operatorServersIndex].ntfServers.toMutableList().apply {
this.removeAt(serverIndex)
}
)
}
}
}
fun deleteXFTPServer(
userServers: MutableState<List<UserOperatorServers>>,
operatorServersIndex: Int,
@@ -137,6 +137,8 @@
<string name="no_media_servers_configured">No media &amp; file servers.</string>
<string name="no_media_servers_configured_for_sending">No servers to send files.</string>
<string name="no_media_servers_configured_for_private_routing">No servers to receive files.</string>
<string name="no_notif_servers_configured">No notification servers.</string>
<string name="no_notif_servers_configured_for_receiving">No servers to notify.</string>
<string name="for_chat_profile">For chat profile %s:</string>
<string name="errors_in_servers_configuration">Errors in servers configuration.</string>
<string name="error_accepting_operator_conditions">Error accepting conditions</string>
@@ -270,9 +272,11 @@
<string name="notifications_mode_off">Runs when app is open</string>
<string name="notifications_mode_periodic">Starts periodically</string>
<string name="notifications_mode_service">Always on</string>
<string name="notifications_mode_instant">Instant</string>
<string name="notifications_mode_off_desc">App can receive notifications only when it\'s running, no background service will be started</string>
<string name="notifications_mode_periodic_desc">Checks new messages every 10 minutes for up to 1 minute</string>
<string name="notifications_mode_service_desc">Background service is always running – notifications will be shown as soon as the messages are available.</string>
<string name="notifications_mode_instant_desc">Receive notifications instantly using an external push service.</string>
<string name="notification_preview_mode_message">Message text</string>
<string name="notification_preview_mode_contact">Contact name</string>
<string name="notification_preview_mode_hidden">Hidden</string>
@@ -284,6 +288,16 @@
<string name="notification_new_contact_request">New contact request</string>
<string name="notification_contact_connected">Connected</string>
<string name="error_showing_desktop_notification">Error showing notification, contact developers.</string>
<string name="warning_push_needs_ntf_server">You first need to set a NTF server to get push notifications.\n\nIt will send the notifications to your push provider.</string>
<string name="warning_push_needs_ntf_server_with_vapid">Your NTF server doesn\'t support VAPID, which is required to get push notifications.</string>
<string name="warning_push_needs_push_service">You don\'t have any push service installed on your device.\n\nPlease installed one and try again.\n\nFor more information, visit\ </string>
<string name="warning_ntf_server_changed">Your NTF server has been removed or has changed and doesn\'t support VAPID, which is required to get push notifications.</string>
<string name="select_push_service">Select Push Service</string>
<string name="select_push_service_intro">Multiple push services are installed on your system, please select the service you wish to use.</string>
<string name="unifiedpush_unregistered">SimpleX is no longer registered with your UnifiedPush distributor. The distributor may have been uninstalled or been logged out. You should reset your notifications settings.</string>
<string name="unifiedpush_registration_failed_network">Registration with your UnifiedPush distributor failed due to a network issue. Please try again when your network is back.</string>
<string name="unifiedpush_registration_failed_action">Registration with your UnifiedPush distributor failed due to a missing requirement from your distributor.</string>
<string name="unifiedpush_registration_failed_unknown">Registration with your UnifiedPush distributor failed due to an unknown issue. This could be a missing requirement from your distributor.</string>
<!-- local authentication notice - SimpleXAPI.kt -->
<string name="la_notice_title_simplex_lock">SimpleX Lock</string>
@@ -949,6 +963,7 @@
<string name="smp_servers_per_user">The servers for new connections of your current chat profile</string>
<string name="smp_save_servers_question">Save servers?</string>
<string name="media_and_file_servers">Media &amp; file servers</string>
<string name="notif_servers">Notifications servers</string>
<string name="xftp_servers">XFTP servers</string>
<string name="xftp_servers_configured">Configured XFTP servers</string>
<string name="xftp_servers_other">Other XFTP servers</string>
@@ -1995,6 +2010,7 @@
<string name="operator_use_for_files">Use for files</string>
<string name="operator_use_for_sending">To send</string>
<string name="xftp_servers_per_user">The servers for new files of your current chat profile</string>
<string name="ntf_servers_per_user">The servers for your notifications.</string>
<string name="operator_added_xftp_servers">Added media &amp; file servers</string>
<string name="operator_open_conditions">Open conditions</string>
<string name="operator_open_changes">Open changes</string>
@@ -2734,6 +2750,7 @@
<string name="servers_info_starting_from">Starting from %s.</string>
<string name="smp_server">SMP server</string>
<string name="xftp_server">XFTP server</string>
<string name="ntf_server">NTF server</string>
<string name="reconnect">Reconnect</string>
<string name="attempts_label">attempts</string>
<string name="sent_directly">Sent directly</string>
+7 -2
View File
@@ -11,8 +11,13 @@ constraints: zip +disable-bzip2 +disable-zstd
source-repository-package
type: git
location: https://github.com/simplex-chat/simplexmq.git
tag: a2d35281b2e20022e75a4dcbecf16a7970d52e8f
location: https://codeberg.org/s1m/sxmq.git
tag: f28a2052ae88c59653993ca0e409231b5ed00b11
-- source-repository-package
-- type: git
-- location: https://github.com/simplex-chat/simplexmq.git
-- tag: d352d518c2b3a42bc7a298954dde799422e1457f
source-repository-package
type: git
+2 -1
View File
@@ -1,5 +1,6 @@
{
"https://github.com/simplex-chat/simplexmq.git"."a2d35281b2e20022e75a4dcbecf16a7970d52e8f" = "0sbq5mmsp8q4p527b286anbjidk2mlfdjsqrbiw90bc1rqwjwpm2";
"https://codeberg.org/s1m/sxmq.git"."f28a2052ae88c59653993ca0e409231b5ed00b11" = "86128d5cdb952e28460b4450042622807e0b683de5598817cac80e85ecee8c3f";
"https://github.com/simplex-chat/simplexmq.git"."d352d518c2b3a42bc7a298954dde799422e1457f" = "1rha84pfpaqx3mf218szkfra334vhijqf17hanxqmp1sicfbf1x3";
"https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38";
"https://github.com/simplex-chat/direct-sqlcipher.git"."f814ee68b16a9447fbb467ccc8f29bdd3546bfd9" = "1ql13f4kfwkbaq7nygkxgw84213i0zm7c1a8hwvramayxl38dq5d";
"https://github.com/simplex-chat/sqlcipher-simple.git"."a46bd361a19376c5211f1058908fc0ae6bf42446" = "1z0r78d8f0812kxbgsm735qf6xx8lvaz27k1a0b4a2m0sshpd5gl";
+1
View File
@@ -255,6 +255,7 @@ library
Simplex.Chat.Store.SQLite.Migrations.M20250729_member_contact_requests
Simplex.Chat.Store.SQLite.Migrations.M20250801_via_group_link_uri
Simplex.Chat.Store.SQLite.Migrations.M20250802_chat_peer_type
Simplex.Chat.Store.SQLite.Migrations.M20250815_extras
other-modules:
Paths_simplex_chat
hs-source-dirs:
+31 -18
View File
@@ -73,17 +73,20 @@ defaultChatConfig =
smp = simplexChatSMPServers,
useSMP = 4,
xftp = map (presetServer True) $ L.toList defaultXFTPServers,
useXFTP = 3
useXFTP = 3,
ntf = map (presetServer True) $ L.toList _defaultNtfServers,
useNTF = 3
},
PresetOperator
{ operator = Just operatorFlux,
smp = fluxSMPServers,
useSMP = 3,
xftp = fluxXFTPServers,
useXFTP = 3
useXFTP = 3,
ntf = fluxNTFServers,
useNTF = 3
}
],
ntf = _defaultNtfServers,
netCfg = defaultNetworkConfig
},
-- please note: if these servers are changed, this option needs to be split to two,
@@ -126,7 +129,7 @@ newChatController
ChatDatabase {chatStore, agentStore}
user
cfg@ChatConfig {agentConfig = aCfg, presetServers, inlineFiles, deviceNameForRemote, confirmMigrations}
ChatOpts {coreOptions = CoreChatOpts {smpServers, xftpServers, simpleNetCfg, logLevel, logConnections, logServerHosts, logFile, tbqSize, deviceName, highlyAvailable, yesToUpMigrations}, optFilesFolder, optTempDirectory, showReactions, allowInstantFiles, autoAcceptFileSize}
ChatOpts {coreOptions = CoreChatOpts {smpServers, xftpServers, ntfServers, simpleNetCfg, logLevel, logConnections, logServerHosts, logFile, tbqSize, deviceName, highlyAvailable, yesToUpMigrations}, optFilesFolder, optTempDirectory, showReactions, allowInstantFiles, autoAcceptFileSize}
backgroundMode = do
let inlineFiles' = if allowInstantFiles || autoAcceptFileSize > 0 then inlineFiles else inlineFiles {sendChunks = 0, receiveInstant = False}
confirmMigrations' = if confirmMigrations == MCConsole && yesToUpMigrations then MCYesUp else confirmMigrations
@@ -139,7 +142,8 @@ newChatController
opDomains = operatorDomains $ mapMaybe operatorWithId $ zip [1 ..] rndSrvs
agentSMP <- randomServerCfgs "agent SMP servers" SPSMP opDomains rndSrvs
agentXFTP <- randomServerCfgs "agent XFTP servers" SPXFTP opDomains rndSrvs
let randomAgentServers = RandomAgentServers {smpServers = agentSMP, xftpServers = agentXFTP}
agentNTF <- randomServerCfgs "agent NTF servers" SPNTF opDomains rndSrvs
let randomAgentServers = RandomAgentServers {smpServers = agentSMP, xftpServers = agentXFTP, ntfServers = agentNTF}
currentRemoteHost <- newTVarIO Nothing
servers <- withTransaction chatStore $ \db -> agentServers db config randomPresetServers randomAgentServers
smpAgent <- getSMPAgentClient aCfg {tbqSize} servers agentStore backgroundMode
@@ -221,47 +225,56 @@ newChatController
where
PresetServers {operators, netCfg} = presetServers
netCfg' = updateNetworkConfig netCfg simpleNetCfg
operators' = case (smpServers, xftpServers) of
([], []) -> operators
(smpSrvs, []) -> L.map disableSMP operators <> [custom smpSrvs []]
([], xftpSrvs) -> L.map disableXFTP operators <> [custom [] xftpSrvs]
(smpSrvs, xftpSrvs) -> [custom smpSrvs xftpSrvs]
operators' = case (smpServers, xftpServers, ntfServers) of
([], [], []) -> operators
(smpSrvs, [], []) -> L.map disableSMP operators <> [custom smpSrvs [] []]
([], xftpSrvs, []) -> L.map disableXFTP operators <> [custom [] xftpSrvs []]
(smpSrvs, xftpSrvs, []) -> L.map disableSMP operators <> L.map disableXFTP operators <> [custom smpSrvs xftpSrvs []]
([], [], ntfSrvs) -> L.map disableNTF operators <> [custom [] [] ntfSrvs]
(smpSrvs, [], ntfSrvs) -> L.map disableSMP operators <> L.map disableNTF operators <> [custom smpSrvs [] ntfSrvs]
([], xftpSrvs, ntfSrvs) -> L.map disableXFTP operators <> L.map disableNTF operators <> [custom [] xftpSrvs ntfSrvs]
(smpSrvs, xftpSrvs, ntfSrvs) -> [custom smpSrvs xftpSrvs ntfSrvs]
disableSMP op@PresetOperator {smp} = (op :: PresetOperator) {smp = map disableSrv smp}
disableXFTP op@PresetOperator {xftp} = (op :: PresetOperator) {xftp = map disableSrv xftp}
disableNTF op@PresetOperator {ntf} = (op :: PresetOperator) {ntf = map disableSrv ntf}
disableSrv :: forall p. NewUserServer p -> NewUserServer p
disableSrv srv = (srv :: NewUserServer p) {enabled = False}
custom smpSrvs xftpSrvs =
custom smpSrvs xftpSrvs ntfSrvs =
PresetOperator
{ operator = Nothing,
smp = map newUserServer smpSrvs,
useSMP = 0,
xftp = map newUserServer xftpSrvs,
useXFTP = 0
useXFTP = 0,
ntf = map newUserServer ntfSrvs,
useNTF = 0
}
randomServerCfgs :: UserProtocol p => String -> SProtocolType p -> [(Text, ServerOperator)] -> [PresetOperator] -> IO (NonEmpty (ServerCfg p))
randomServerCfgs name p opDomains rndSrvs =
toJustOrError name $ L.nonEmpty $ agentServerCfgs p opDomains $ concatMap (pServers p) rndSrvs
agentServers :: DB.Connection -> ChatConfig -> NonEmpty PresetOperator -> RandomAgentServers -> IO InitialAgentServers
agentServers db ChatConfig {presetServers = PresetServers {ntf, netCfg}, presetDomains} presetOps as = do
agentServers db ChatConfig {presetServers = PresetServers {netCfg}, presetDomains} presetOps as = do
users <- getUsers db
ops <- getUpdateServerOperators db presetOps (null users)
let opDomains = operatorDomains $ mapMaybe snd ops
(smp', xftp') <- unzip <$> mapM (getServers ops opDomains) users
pure InitialAgentServers {smp = M.fromList (optServers smp' smpServers), xftp = M.fromList (optServers xftp' xftpServers), ntf, netCfg, presetDomains}
(smp', xftp', ntf') <- unzip3 <$> mapM (getServers ops opDomains) users
pure InitialAgentServers {smp = M.fromList (optServers smp' smpServers), xftp = M.fromList (optServers xftp' xftpServers), ntf = M.fromList (optServers ntf' ntfServers), netCfg, presetDomains}
where
optServers :: [(UserId, NonEmpty (ServerCfg p))] -> [ProtoServerWithAuth p] -> [(UserId, NonEmpty (ServerCfg p))]
optServers srvs overrides_ = case L.nonEmpty overrides_ of
Just overrides -> map (second $ const $ L.map (presetServerCfg True allRoles Nothing) overrides) srvs
Nothing -> srvs
getServers :: [(Maybe PresetOperator, Maybe ServerOperator)] -> [(Text, ServerOperator)] -> User -> IO ((UserId, NonEmpty (ServerCfg 'PSMP)), (UserId, NonEmpty (ServerCfg 'PXFTP)))
getServers :: [(Maybe PresetOperator, Maybe ServerOperator)] -> [(Text, ServerOperator)] -> User -> IO ((UserId, NonEmpty (ServerCfg 'PSMP)), (UserId, NonEmpty (ServerCfg 'PXFTP)), (UserId, NonEmpty (ServerCfg 'PNTF)))
getServers ops opDomains user' = do
smpSrvs <- getProtocolServers db SPSMP user'
xftpSrvs <- getProtocolServers db SPXFTP user'
uss <- groupByOperator' (ops, smpSrvs, xftpSrvs)
ntfSrvs <- getProtocolServers db SPNTF user'
uss <- groupByOperator' (ops, smpSrvs, xftpSrvs, ntfSrvs)
ts <- getCurrentTime
uss' <- mapM (setUserServers' db user' ts . updatedUserServers) uss
let auId = aUserId user'
pure $ bimap (auId,) (auId,) $ useServers as opDomains uss'
pure $ auIdMap auId $ useServers as opDomains uss'
auIdMap auId (a, b, c) = ((auId, a), (auId, b), (auId, c))
chooseRandomServers :: PresetServers -> IO (NonEmpty PresetOperator)
chooseRandomServers PresetServers {operators} =
+4 -2
View File
@@ -162,7 +162,8 @@ data ChatConfig = ChatConfig
data RandomAgentServers = RandomAgentServers
{ smpServers :: NonEmpty (ServerCfg 'PSMP),
xftpServers :: NonEmpty (ServerCfg 'PXFTP)
xftpServers :: NonEmpty (ServerCfg 'PXFTP),
ntfServers :: NonEmpty (ServerCfg 'PNTF)
}
deriving (Show)
@@ -184,7 +185,6 @@ defaultChatHooks = ChatHooks Nothing Nothing Nothing
data PresetServers = PresetServers
{ operators :: NonEmpty PresetOperator,
ntf :: [NtfServer],
netCfg :: NetworkConfig
}
deriving (Show)
@@ -354,8 +354,10 @@ data ChatCommand
| APIGetNtfToken
| APIRegisterToken DeviceToken NotificationsMode
| APIVerifyToken DeviceToken C.CbNonce ByteString
| APIVerifySavedToken ByteString
| APICheckToken DeviceToken
| APIDeleteToken DeviceToken
| APIDeleteSavedToken
| APIGetNtfConns {nonce :: C.CbNonce, encNtfInfo :: ByteString}
| APIGetConnNtfMessages (NonEmpty ConnMsgReq)
| APIAddMember {groupId :: GroupId, contactId :: ContactId, memberRole :: GroupMemberRole}
+38 -24
View File
@@ -103,7 +103,7 @@ import qualified Simplex.Messaging.Crypto.File as CF
import Simplex.Messaging.Crypto.Ratchet (PQEncryption (..), PQSupport (..), pattern IKPQOff, pattern IKPQOn, pattern PQEncOff, pattern PQSupportOff, pattern PQSupportOn)
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Parsers (base64P)
import Simplex.Messaging.Protocol (AProtoServerWithAuth (..), AProtocolType (..), MsgFlags (..), NtfServer, ProtoServerWithAuth (..), ProtocolServer, ProtocolType (..), ProtocolTypeI (..), SProtocolType (..), SubscriptionMode (..), UserProtocol, userProtocol)
import Simplex.Messaging.Protocol (AProtoServerWithAuth (..), AProtocolType (..), MsgFlags (..), NtfServer, ProtoServerWithAuth (..), ProtocolServer, ProtocolType (..), ProtocolTypeI (..), SProtocolType (..), SubscriptionMode (..), UserProtocol, userProtocol, NtfServerWithAuth)
import qualified Simplex.Messaging.Protocol as SMP
import Simplex.Messaging.ServiceScheme (ServiceScheme (..))
import qualified Simplex.Messaging.TMap as TM
@@ -135,7 +135,7 @@ import Simplex.Messaging.Agent.Store.Common (withConnection)
import Simplex.Messaging.Agent.Store.SQLite.DB (SlowQueryStats (..))
#endif
_defaultNtfServers :: [NtfServer]
_defaultNtfServers :: NonEmpty NtfServerWithAuth
_defaultNtfServers =
[ -- "ntf://FB-Uop7RTaZZEG0ZLD2CIaTjsPh-Fw0zFAnb7QyA8Ks=@ntf2.simplex.im,5ex3mupcazy3zlky64ab27phjhijpemsiby33qzq3pliejipbtx5xgad.onion"
"ntf://KmpZNNXiVZJx_G2T7jRUmDFxWXM3OAnunz3uLT0tqAA=@ntf3.simplex.im,pxculznuryunjdvtvh6s6szmanyadumpbmvevgdpe4wk5c65unyt4yid.onion",
@@ -267,11 +267,12 @@ updateNetworkConfig cfg SimpleNetCfg {socksProxy, socksMode, hostMode, requiredH
nt t = NetworkTimeout {backgroundTimeout = t * 3, interactiveTimeout = t}
in cfg3 {socksProxy, socksMode, hostMode, requiredHostMode, smpWebPortServers, logTLSErrors}
useServers :: Foldable f => RandomAgentServers -> [(Text, ServerOperator)] -> f UserOperatorServers -> (NonEmpty (ServerCfg 'PSMP), NonEmpty (ServerCfg 'PXFTP))
useServers :: Foldable f => RandomAgentServers -> [(Text, ServerOperator)] -> f UserOperatorServers -> (NonEmpty (ServerCfg 'PSMP), NonEmpty (ServerCfg 'PXFTP), NonEmpty (ServerCfg 'PNTF))
useServers as opDomains uss =
let smp' = useServerCfgs SPSMP as opDomains $ concatMap (servers' SPSMP) uss
xftp' = useServerCfgs SPXFTP as opDomains $ concatMap (servers' SPXFTP) uss
in (smp', xftp')
ntf' = useServerCfgs SPNTF as opDomains $ concatMap (servers' SPNTF) uss
in (smp', xftp', ntf')
execChatCommand :: Maybe RemoteHostId -> ByteString -> Int -> CM' (Either ChatError ChatResponse)
execChatCommand rh s retryNum =
@@ -318,8 +319,8 @@ processChatCommand vr nm = \case
forM_ users $ \User {localDisplayName = n, activeUser, viewPwdHash} ->
when (n == displayName) . throwChatError $
if activeUser || isNothing viewPwdHash then CEUserExists displayName else CEInvalidDisplayName {displayName, validName = ""}
(uss, (smp', xftp')) <- chooseServers =<< readTVarIO u
auId <- withAgent $ \a -> createUser a smp' xftp'
(uss, (smp', xftp', ntf')) <- chooseServers =<< readTVarIO u
auId <- withAgent $ \a -> createUser a smp' xftp' ntf'
ts <- liftIO $ getCurrentTime >>= if pastTimestamp then coupleDaysAgo else pure
user <- withFastStore $ \db -> do
user <- createUserRecordAt db (AgentUserId auId) p True ts
@@ -334,7 +335,7 @@ processChatCommand vr nm = \case
createPresetContactCards db user = do
createContact db user simplexStatusContactProfile
createContact db user simplexTeamContactProfile
chooseServers :: Maybe User -> CM ([UpdatedUserOperatorServers], (NonEmpty (ServerCfg 'PSMP), NonEmpty (ServerCfg 'PXFTP)))
chooseServers :: Maybe User -> CM ([UpdatedUserOperatorServers], (NonEmpty (ServerCfg 'PSMP), NonEmpty (ServerCfg 'PXFTP), NonEmpty (ServerCfg 'PNTF)))
chooseServers user_ = do
as <- asks randomAgentServers
mapM (withFastStore . flip getUserServers >=> liftIO . groupByOperator) user_ >>= \case
@@ -345,12 +346,12 @@ processChatCommand vr nm = \case
Nothing -> do
ps <- asks randomPresetServers
uss <- presetUserServers <$> withFastStore' (\db -> getUpdateServerOperators db ps True)
let RandomAgentServers {smpServers = smp', xftpServers = xftp'} = as
pure (uss, (smp', xftp'))
let RandomAgentServers {smpServers = smp', xftpServers = xftp', ntfServers = ntf'} = as
pure (uss, (smp', xftp', ntf'))
copyServers :: UserOperatorServers -> UpdatedUserOperatorServers
copyServers UserOperatorServers {operator, smpServers, xftpServers} =
copyServers UserOperatorServers {operator, smpServers, xftpServers, ntfServers} =
let new srv = AUS SDBNew srv {serverId = DBNewEntity}
in UpdatedUserOperatorServers {operator, smpServers = map new smpServers, xftpServers = map new xftpServers}
in UpdatedUserOperatorServers {operator, smpServers = map new smpServers, xftpServers = map new xftpServers, ntfServers = map new ntfServers}
coupleDaysAgo t = (`addUTCTime` t) . fromInteger . negate . (+ (2 * day)) <$> randomRIO (0, day)
day = 86400
ListUsers -> CRUsersList <$> withFastStore' getUsersInfo
@@ -1350,9 +1351,11 @@ processChatCommand vr nm = \case
APIRegisterToken token mode -> withUser $ \_ ->
CRNtfTokenStatus <$> withAgent (\a -> registerNtfToken a nm token mode)
APIVerifyToken token nonce code -> withUser $ \_ -> withAgent (\a -> verifyNtfToken a nm token nonce code) >> ok_
APIVerifySavedToken code -> withUser $ \_u@User {userId} -> withAgent (\a -> verifySavedNtfToken a userId code) >> ok_
APICheckToken token -> withUser $ \_ ->
CRNtfTokenStatus <$> withAgent (\a -> checkNtfToken a nm token)
APIDeleteToken token -> withUser $ \_ -> withAgent (`deleteNtfToken` token) >> ok_
APIDeleteSavedToken -> withUser $ \_ -> withAgent deleteSavedNtfToken >> ok_
APIGetNtfConns nonce encNtfInfo -> withUser $ \_ -> do
ntfInfos <- withAgent $ \a -> getNotificationConns a nonce encNtfInfo
(errs, ntfMsgs) <- lift $ partitionEithers <$> withStoreBatch' (\db -> map (getMsgConn db) (L.toList ntfInfos))
@@ -1400,16 +1403,18 @@ processChatCommand vr nm = \case
ops' = map Just ops <> [Nothing]
opDomains = operatorDomains ops
liftIO $ fmap (opsConds,) . mapM (getServers db as ops' opDomains) =<< getUsers db
lift $ withAgent' $ \a -> forM_ srvs $ \(auId, (smp', xftp')) -> do
lift $ withAgent' $ \a -> forM_ srvs $ \(auId, (smp', xftp', ntf')) -> do
setProtocolServers a auId smp'
setProtocolServers a auId xftp'
setProtocolServers a auId ntf'
pure $ CRServerOperatorConditions opsConds
where
getServers :: DB.Connection -> RandomAgentServers -> [Maybe ServerOperator] -> [(Text, ServerOperator)] -> User -> IO (UserId, (NonEmpty (ServerCfg 'PSMP), NonEmpty (ServerCfg 'PXFTP)))
getServers :: DB.Connection -> RandomAgentServers -> [Maybe ServerOperator] -> [(Text, ServerOperator)] -> User -> IO (UserId, (NonEmpty (ServerCfg 'PSMP), NonEmpty (ServerCfg 'PXFTP), NonEmpty (ServerCfg 'PNTF)))
getServers db as ops opDomains user = do
smpSrvs <- getProtocolServers db SPSMP user
xftpSrvs <- getProtocolServers db SPXFTP user
uss <- groupByOperator (ops, smpSrvs, xftpSrvs)
ntfSrvs <- getProtocolServers db SPNTF user
uss <- groupByOperator (ops, smpSrvs, xftpSrvs, ntfSrvs)
pure $ (aUserId user,) $ useServers as opDomains uss
SetServerOperators operatorsRoles -> do
ops <- serverOperators <$> withFastStore getServerOperators
@@ -1433,9 +1438,10 @@ processChatCommand vr nm = \case
lift $ withAgent' $ \a -> do
let auId = aUserId user
opDomains = operatorDomains $ mapMaybe operator' $ L.toList uss
(smp', xftp') = useServers as opDomains uss
(smp', xftp', ntf') = useServers as opDomains uss
setProtocolServers a auId smp'
setProtocolServers a auId xftp'
setProtocolServers a auId ntf'
ok_
APIValidateServers userId userServers -> withUserId userId $ \user ->
CRUserServersValidation user <$> validateAllUsersServers userId userServers
@@ -2913,7 +2919,8 @@ processChatCommand vr nm = \case
users <- getUsers db
smpServers <- getServers db user SPSMP
xftpServers <- getServers db user SPXFTP
let presentedServersSummary = toPresentedServersSummary agentServersSummary users user smpServers xftpServers _defaultNtfServers
ntfServers <- getServers db user SPNTF
let presentedServersSummary = toPresentedServersSummary agentServersSummary users user smpServers xftpServers ntfServers
pure $ CRAgentServersSummary user presentedServersSummary
where
getServers :: ProtocolTypeI p => DB.Connection -> User -> SProtocolType p -> IO [ProtocolServer p]
@@ -3986,18 +3993,21 @@ data ConnectViaContactResult
= CVRConnectedContact Contact
| CVRSentInvitation Connection (Maybe Profile)
protocolServers :: UserProtocol p => SProtocolType p -> ([Maybe ServerOperator], [UserServer 'PSMP], [UserServer 'PXFTP]) -> ([Maybe ServerOperator], [UserServer 'PSMP], [UserServer 'PXFTP])
protocolServers p (operators, smpServers, xftpServers) = case p of
SPSMP -> (operators, smpServers, [])
SPXFTP -> (operators, [], xftpServers)
protocolServers :: UserProtocol p => SProtocolType p -> ([Maybe ServerOperator], [UserServer 'PSMP], [UserServer 'PXFTP], [UserServer 'PNTF]) -> ([Maybe ServerOperator], [UserServer 'PSMP], [UserServer 'PXFTP], [UserServer 'PNTF])
protocolServers p (operators, smpServers, xftpServers, ntfServers) = case p of
SPSMP -> (operators, smpServers, [], [])
SPXFTP -> (operators, [], xftpServers, [])
SPNTF -> (operators, [], [], ntfServers)
-- disable preset and replace custom servers (groupByOperator always adds custom)
updatedServers :: forall p. UserProtocol p => SProtocolType p -> [AUserServer p] -> UserOperatorServers -> UpdatedUserOperatorServers
updatedServers p' srvs UserOperatorServers {operator, smpServers, xftpServers} = case p' of
SPSMP -> u (updateSrvs smpServers, map (AUS SDBStored) xftpServers)
SPXFTP -> u (map (AUS SDBStored) smpServers, updateSrvs xftpServers)
updatedServers p' srvs UserOperatorServers {operator, smpServers, xftpServers, ntfServers} = case p' of
SPSMP -> u (updateSrvs smpServers, map (AUS SDBStored) xftpServers, map (AUS SDBStored) ntfServers)
SPXFTP -> u (map (AUS SDBStored) smpServers, updateSrvs xftpServers, map (AUS SDBStored) ntfServers)
SPNTF -> u (map (AUS SDBStored) smpServers, map (AUS SDBStored) xftpServers, updateSrvs ntfServers)
where
u = uncurry $ UpdatedUserOperatorServers operator
u :: ([AUserServer 'PSMP], [AUserServer 'PXFTP], [AUserServer 'PNTF]) -> UpdatedUserOperatorServers
u (smp, xftp, ntf) = UpdatedUserOperatorServers operator smp xftp ntf
updateSrvs :: [UserServer p] -> [AUserServer p]
updateSrvs pSrvs = map disableSrv pSrvs <> maybe srvs (const []) operator
disableSrv srv@UserServer {preset} =
@@ -4502,8 +4512,10 @@ chatCommandP =
"/_ntf get" $> APIGetNtfToken,
"/_ntf register " *> (APIRegisterToken <$> strP_ <*> strP),
"/_ntf verify " *> (APIVerifyToken <$> strP <* A.space <*> strP <* A.space <*> strP),
"/_ntf verify " *> (APIVerifySavedToken <$> strP),
"/_ntf check " *> (APICheckToken <$> strP),
"/_ntf delete " *> (APIDeleteToken <$> strP),
"/_ntf delete saved" $> APIDeleteSavedToken,
"/_ntf conns " *> (APIGetNtfConns <$> strP <* A.space <*> strP),
"/_ntf conn messages " *> (APIGetConnNtfMessages <$> connMsgsP),
"/_add #" *> (APIAddMember <$> A.decimal <* A.space <*> A.decimal <*> memberRole),
@@ -4523,8 +4535,10 @@ chatCommandP =
"/ntf test " *> (TestProtoServer . AProtoServerWithAuth SPNTF <$> strP),
"/smp " *> (SetUserProtoServers (AProtocolType SPSMP) . map (AProtoServerWithAuth SPSMP) <$> protocolServersP),
"/xftp " *> (SetUserProtoServers (AProtocolType SPXFTP) . map (AProtoServerWithAuth SPXFTP) <$> protocolServersP),
"/ntf " *> (SetUserProtoServers (AProtocolType SPNTF) . map (AProtoServerWithAuth SPNTF) <$> protocolServersP),
"/smp" $> GetUserProtoServers (AProtocolType SPSMP),
"/xftp" $> GetUserProtoServers (AProtocolType SPXFTP),
"/ntf" $> GetUserProtoServers (AProtocolType SPNTF),
"/_operators" $> APIGetServerOperators,
"/_operators " *> (APISetServerOperators <$> jsonP),
"/operators " *> (SetServerOperators . L.fromList <$> operatorRolesP `A.sepBy1` A.char ','),
+2 -1
View File
@@ -151,13 +151,14 @@ withFileLock name = withEntityLock name . CLFile
{-# INLINE withFileLock #-}
useServerCfgs :: forall p. UserProtocol p => SProtocolType p -> RandomAgentServers -> [(Text, ServerOperator)] -> [UserServer p] -> NonEmpty (ServerCfg p)
useServerCfgs p RandomAgentServers {smpServers, xftpServers} opDomains =
useServerCfgs p RandomAgentServers {smpServers, xftpServers, ntfServers} opDomains =
fromMaybe (rndAgentServers p) . L.nonEmpty . agentServerCfgs p opDomains
where
rndAgentServers :: SProtocolType p -> NonEmpty (ServerCfg p)
rndAgentServers = \case
SPSMP -> smpServers
SPXFTP -> xftpServers
SPNTF -> ntfServers
contactCITimed :: Contact -> CM (Maybe CITimed)
contactCITimed ct = sndContactCITimed False ct Nothing
+1
View File
@@ -245,6 +245,7 @@ mobileChatOpts dbOptions =
{ dbOptions,
smpServers = [],
xftpServers = [],
ntfServers = [],
simpleNetCfg = defaultSimpleNetCfg,
logLevel = CLLImportant,
logConnections = False,
+31 -20
View File
@@ -172,6 +172,7 @@ operatorRoles :: UserProtocol p => SProtocolType p -> ServerOperator -> ServerRo
operatorRoles p op = case p of
SPSMP -> smpRoles op
SPXFTP -> xftpRoles op
SPNTF -> ServerRoles{proxy = False, storage = False}
conditionsAccepted :: ServerOperator -> Bool
conditionsAccepted ServerOperator {conditionsAcceptance} = case conditionsAcceptance of
@@ -181,14 +182,16 @@ conditionsAccepted ServerOperator {conditionsAcceptance} = case conditionsAccept
data UserOperatorServers = UserOperatorServers
{ operator :: Maybe ServerOperator,
smpServers :: [UserServer 'PSMP],
xftpServers :: [UserServer 'PXFTP]
xftpServers :: [UserServer 'PXFTP],
ntfServers :: [UserServer 'PNTF]
}
deriving (Show)
data UpdatedUserOperatorServers = UpdatedUserOperatorServers
{ operator :: Maybe ServerOperator,
smpServers :: [AUserServer 'PSMP],
xftpServers :: [AUserServer 'PXFTP]
xftpServers :: [AUserServer 'PXFTP],
ntfServers :: [AUserServer 'PNTF]
}
deriving (Show)
@@ -205,17 +208,19 @@ instance UserServersClass UserOperatorServers where
type AServer UserOperatorServers = UserServer' 'DBStored
operator' UserOperatorServers {operator} = operator
aUserServer' = AUS SDBStored
servers' p UserOperatorServers {smpServers, xftpServers} = case p of
servers' p UserOperatorServers {smpServers, xftpServers, ntfServers} = case p of
SPSMP -> smpServers
SPXFTP -> xftpServers
SPNTF -> ntfServers
instance UserServersClass UpdatedUserOperatorServers where
type AServer UpdatedUserOperatorServers = AUserServer
operator' UpdatedUserOperatorServers {operator} = operator
aUserServer' = id
servers' p UpdatedUserOperatorServers {smpServers, xftpServers} = case p of
servers' p UpdatedUserOperatorServers {smpServers, xftpServers, ntfServers} = case p of
SPSMP -> smpServers
SPXFTP -> xftpServers
SPNTF -> ntfServers
type UserServer p = UserServer' 'DBStored p
@@ -244,7 +249,9 @@ data PresetOperator = PresetOperator
smp :: [NewUserServer 'PSMP],
useSMP :: Int,
xftp :: [NewUserServer 'PXFTP],
useXFTP :: Int
useXFTP :: Int,
ntf :: [NewUserServer 'PNTF],
useNTF :: Int
}
deriving (Show)
@@ -252,14 +259,16 @@ pOperator :: PresetOperator -> Maybe NewServerOperator
pOperator PresetOperator {operator} = operator
pServers :: UserProtocol p => SProtocolType p -> PresetOperator -> [NewUserServer p]
pServers p PresetOperator {smp, xftp} = case p of
pServers p PresetOperator {smp, xftp, ntf} = case p of
SPSMP -> smp
SPXFTP -> xftp
SPNTF -> ntf
operatorServersToUse :: UserProtocol p => SProtocolType p -> PresetOperator -> Int
operatorServersToUse p PresetOperator {useSMP, useXFTP} = case p of
operatorServersToUse p PresetOperator {useSMP, useXFTP, useNTF} = case p of
SPSMP -> useSMP
SPXFTP -> useXFTP
SPNTF -> useNTF
presetServer' :: Bool -> ProtocolServer p -> NewUserServer p
presetServer' enabled = presetServer enabled . (`ProtoServerWithAuth` Nothing)
@@ -301,8 +310,8 @@ usageConditionsToAdd' prevCommit sourceCommit newUser createdAt = \case
presetUserServers :: [(Maybe PresetOperator, Maybe ServerOperator)] -> [UpdatedUserOperatorServers]
presetUserServers = mapMaybe $ \(presetOp_, op) -> mkUS op <$> presetOp_
where
mkUS op PresetOperator {smp, xftp} =
UpdatedUserOperatorServers op (map (AUS SDBNew) smp) (map (AUS SDBNew) xftp)
mkUS op PresetOperator {smp, xftp, ntf} =
UpdatedUserOperatorServers op (map (AUS SDBNew) smp) (map (AUS SDBNew) xftp) (map (AUS SDBNew) ntf)
-- This function should be used inside DB transaction to update operators.
-- It allows to add/remove/update preset operators in the database preserving enabled and roles settings,
@@ -322,13 +331,13 @@ updatedServerOperators presetOps storedOps =
-- This function should be used inside DB transaction to update servers.
updatedUserServers :: (Maybe PresetOperator, UserOperatorServers) -> UpdatedUserOperatorServers
updatedUserServers (presetOp_, UserOperatorServers {operator, smpServers, xftpServers}) =
UpdatedUserOperatorServers {operator, smpServers = smp', xftpServers = xftp'}
updatedUserServers (presetOp_, UserOperatorServers {operator, smpServers, xftpServers, ntfServers}) =
UpdatedUserOperatorServers {operator, smpServers = smp', xftpServers = xftp', ntfServers = ntf'}
where
stored = map (AUS SDBStored)
(smp', xftp') = case presetOp_ of
Nothing -> (stored smpServers, stored xftpServers)
Just presetOp -> (updated SPSMP smpServers, updated SPXFTP xftpServers)
(smp', xftp', ntf') = case presetOp_ of
Nothing -> (stored smpServers, stored xftpServers, stored ntfServers)
Just presetOp -> (updated SPSMP smpServers, updated SPXFTP xftpServers, updated SPNTF ntfServers)
where
updated :: forall p. UserProtocol p => SProtocolType p -> [UserServer p] -> [AUserServer p]
updated p srvs = map userServer presetSrvs <> stored (filter customServer srvs)
@@ -379,34 +388,36 @@ instance Box ((,) (Maybe a)) where
box = (Nothing,)
unbox = snd
groupByOperator :: ([Maybe ServerOperator], [UserServer 'PSMP], [UserServer 'PXFTP]) -> IO [UserOperatorServers]
groupByOperator (ops, smpSrvs, xftpSrvs) = map runIdentity <$> groupByOperator_ (map Identity ops, smpSrvs, xftpSrvs)
groupByOperator :: ([Maybe ServerOperator], [UserServer 'PSMP], [UserServer 'PXFTP], [UserServer 'PNTF]) -> IO [UserOperatorServers]
groupByOperator (ops, smpSrvs, xftpSrvs, ntfSrvs) = map runIdentity <$> groupByOperator_ (map Identity ops, smpSrvs, xftpSrvs, ntfSrvs)
-- For the initial app start this function relies on tuple being Functor/Box
-- to preserve the information about operator being DBNew or DBStored
groupByOperator' :: ([(Maybe PresetOperator, Maybe ServerOperator)], [UserServer 'PSMP], [UserServer 'PXFTP]) -> IO [(Maybe PresetOperator, UserOperatorServers)]
groupByOperator' :: ([(Maybe PresetOperator, Maybe ServerOperator)], [UserServer 'PSMP], [UserServer 'PXFTP], [UserServer 'PNTF]) -> IO [(Maybe PresetOperator, UserOperatorServers)]
groupByOperator' = groupByOperator_
{-# INLINE groupByOperator' #-}
groupByOperator_ :: forall f. (Box f, Traversable f) => ([f (Maybe ServerOperator)], [UserServer 'PSMP], [UserServer 'PXFTP]) -> IO [f UserOperatorServers]
groupByOperator_ (ops, smpSrvs, xftpSrvs) = do
groupByOperator_ :: forall f. (Box f, Traversable f) => ([f (Maybe ServerOperator)], [UserServer 'PSMP], [UserServer 'PXFTP], [UserServer 'PNTF]) -> IO [f UserOperatorServers]
groupByOperator_ (ops, smpSrvs, xftpSrvs, ntfSrvs) = do
let ops' = mapMaybe sequence ops
customOp_ = find (isNothing . unbox) ops
ss <- mapM ((\op -> (serverDomains (unbox op),) <$> newIORef (mkUS . Just <$> op))) ops'
custom <- newIORef $ maybe (box $ mkUS Nothing) (mkUS <$>) customOp_
mapM_ (addServer ss custom addSMP) (reverse smpSrvs)
mapM_ (addServer ss custom addXFTP) (reverse xftpSrvs)
mapM_ (addServer ss custom addNTF) (reverse ntfSrvs)
opSrvs <- mapM (readIORef . snd) ss
customSrvs <- readIORef custom
pure $ opSrvs <> [customSrvs]
where
mkUS op = UserOperatorServers op [] []
mkUS op = UserOperatorServers op [] [] []
addServer :: [([Text], IORef (f UserOperatorServers))] -> IORef (f UserOperatorServers) -> (UserServer p -> UserOperatorServers -> UserOperatorServers) -> UserServer p -> IO ()
addServer ss custom add srv =
let v = maybe custom snd $ find (\(ds, _) -> any (\d -> any (matchingHost d) (srvHost srv)) ds) ss
in atomicModifyIORef'_ v (add srv <$>)
addSMP srv s@UserOperatorServers {smpServers} = (s :: UserOperatorServers) {smpServers = srv : smpServers}
addXFTP srv s@UserOperatorServers {xftpServers} = (s :: UserOperatorServers) {xftpServers = srv : xftpServers}
addNTF srv s@UserOperatorServers {ntfServers} = (s :: UserOperatorServers) {ntfServers = srv : ntfServers}
data UserServersError
= USENoServers {protocol :: AProtocolType, user :: Maybe User}
+3
View File
@@ -116,3 +116,6 @@ fluxXFTPServers =
"xftp://Rh19D5e4Eez37DEE9hAlXDB3gZa1BdFYJTPgJWPO9OI=@xftp5.simplexonflux.com,q7itltdn32hjmgcqwhow4tay5ijetng3ur32bolssw32fvc5jrwvozad.onion",
"xftp://0AznwoyfX8Od9T_acp1QeeKtxUi676IBIiQjXVwbdyU=@xftp6.simplexonflux.com,upvzf23ou6nrmaf3qgnhd6cn3d74tvivlmz3p7wdfwq6fhthjrjiiqid.onion"
]
fluxNTFServers :: [NewUserServer 'PNTF]
fluxNTFServers = []
+15 -1
View File
@@ -33,7 +33,7 @@ import Simplex.FileTransfer.Description (mb)
import Simplex.Messaging.Client (HostMode (..), SMPWebPortServers (..), SocksMode (..), textToHostMode)
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Parsers (parseAll)
import Simplex.Messaging.Protocol (ProtoServerWithAuth, ProtocolTypeI, SMPServerWithAuth, XFTPServerWithAuth)
import Simplex.Messaging.Protocol (ProtoServerWithAuth, ProtocolTypeI, SMPServerWithAuth, XFTPServerWithAuth, NtfServerWithAuth)
import Simplex.Messaging.Transport.Client (SocksProxyWithAuth (..), SocksAuth (..), defaultSocksProxyWithAuth)
import Simplex.Chat.Options.DB
@@ -58,6 +58,7 @@ data CoreChatOpts = CoreChatOpts
{ dbOptions :: ChatDbOpts,
smpServers :: [SMPServerWithAuth],
xftpServers :: [XFTPServerWithAuth],
ntfServers :: [NtfServerWithAuth],
simpleNetCfg :: SimpleNetCfg,
logLevel :: ChatLogLevel,
logConnections :: Bool,
@@ -114,6 +115,18 @@ coreChatOptsP appDir defaultDbName = do
)
<> value []
)
ntfServers <-
option
parseProtocolServers
( long "ntf-server"
<> metavar "SERVER"
<> help
( ("Space-separated list of NTF server(s) to use (each server can have more than one hostname)." <> "\n")
<> ("If you pass multiple servers, surround the entire list in quotes." <> "\n")
<> "Examples: ntf1.example.com, \"ntf1.example.com ntf2.example.com ntf3.example.com\""
)
<> value []
)
socksProxy <-
flag' (Just defaultSocksProxyWithAuth) (short 'x' <> help "Use local SOCKS5 proxy at :9050")
<|> option
@@ -248,6 +261,7 @@ coreChatOptsP appDir defaultDbName = do
{ dbOptions,
smpServers,
xftpServers,
ntfServers,
simpleNetCfg =
SimpleNetCfg
{ socksProxy,
+17 -15
View File
@@ -114,7 +114,7 @@ import qualified Simplex.Messaging.Crypto as C
import qualified Simplex.Messaging.Crypto.Ratchet as CR
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Parsers (defaultJSON)
import Simplex.Messaging.Protocol (BasicAuth (..), ProtoServerWithAuth (..), ProtocolServer (..), ProtocolType (..), ProtocolTypeI (..), SProtocolType (..), SubscriptionMode)
import Simplex.Messaging.Protocol (BasicAuth (..), ProtoServerWithAuth (..), ProtocolServer (..), ProtocolType (..), ProtocolTypeI (..), SProtocolType (..), SubscriptionMode, Extras)
import Simplex.Messaging.Agent.Store.Entity
import Simplex.Messaging.Transport.Client (TransportHost)
import Simplex.Messaging.Util (eitherToMaybe, safeDecodeUtf8)
@@ -589,15 +589,15 @@ getProtocolServers db p User {userId} =
<$> DB.query
db
[sql|
SELECT smp_server_id, host, port, key_hash, basic_auth, preset, tested, enabled
SELECT smp_server_id, host, port, key_hash, extras, basic_auth, preset, tested, enabled
FROM protocol_servers
WHERE user_id = ? AND protocol = ?
|]
(userId, decodeLatin1 $ strEncode p)
where
toUserServer :: (DBEntityId, NonEmpty TransportHost, String, C.KeyHash, Maybe Text, BoolInt, Maybe BoolInt, BoolInt) -> UserServer p
toUserServer (serverId, host, port, keyHash, auth_, BI preset, tested, BI enabled) =
let server = ProtoServerWithAuth (ProtocolServer p host port keyHash) (BasicAuth . encodeUtf8 <$> auth_)
toUserServer :: (DBEntityId, NonEmpty TransportHost, String, C.KeyHash, Maybe Extras, Maybe Text, BoolInt, Maybe BoolInt, BoolInt) -> UserServer p
toUserServer (serverId, host, port, keyHash, extras, auth_, BI preset, tested, BI enabled) =
let server = ProtoServerWithAuth (ProtocolServer p host port keyHash extras) (BasicAuth . encodeUtf8 <$> auth_)
in UserServer {serverId, server, preset, tested = unBI <$> tested, enabled, deleted = False}
insertProtocolServer :: forall p. ProtocolTypeI p => DB.Connection -> SProtocolType p -> User -> UTCTime -> NewUserServer p -> IO (UserServer p)
@@ -606,8 +606,8 @@ insertProtocolServer db p User {userId} ts srv@UserServer {server, preset, teste
db
[sql|
INSERT INTO protocol_servers
(protocol, host, port, key_hash, basic_auth, preset, tested, enabled, user_id, created_at, updated_at)
VALUES (?,?,?,?,?,?,?,?,?,?,?)
(protocol, host, port, key_hash, extras, basic_auth, preset, tested, enabled, user_id, created_at, updated_at)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?)
|]
(serverColumns p server :. (BI preset, BI <$> tested, BI enabled, userId, ts, ts))
sId <- insertedRowId db
@@ -619,17 +619,17 @@ updateProtocolServer db p ts UserServer {serverId, server, preset, tested, enabl
db
[sql|
UPDATE protocol_servers
SET protocol = ?, host = ?, port = ?, key_hash = ?, basic_auth = ?,
SET protocol = ?, host = ?, port = ?, key_hash = ?, extras = ?, basic_auth = ?,
preset = ?, tested = ?, enabled = ?, updated_at = ?
WHERE smp_server_id = ?
|]
(serverColumns p server :. (BI preset, BI <$> tested, BI enabled, ts, serverId))
serverColumns :: ProtocolTypeI p => SProtocolType p -> ProtoServerWithAuth p -> (Text, NonEmpty TransportHost, String, C.KeyHash, Maybe Text)
serverColumns p (ProtoServerWithAuth ProtocolServer {host, port, keyHash} auth_) =
serverColumns :: ProtocolTypeI p => SProtocolType p -> ProtoServerWithAuth p -> (Text, NonEmpty TransportHost, String, C.KeyHash, Maybe Extras, Maybe Text)
serverColumns p (ProtoServerWithAuth ProtocolServer {host, port, keyHash, extras} auth_) =
let protocol = decodeLatin1 $ strEncode p
auth = safeDecodeUtf8 . unBasicAuth <$> auth_
in (protocol, host, port, keyHash, auth)
in (protocol, host, port, keyHash, extras, auth)
getServerOperators :: DB.Connection -> ExceptT StoreError IO ServerOperatorConditions
getServerOperators db = do
@@ -642,12 +642,13 @@ getServerOperators db = do
let conditionsAction = usageConditionsAction ops currentConditions now
pure ServerOperatorConditions {serverOperators = ops, currentConditions, conditionsAction}
getUserServers :: DB.Connection -> User -> ExceptT StoreError IO ([Maybe ServerOperator], [UserServer 'PSMP], [UserServer 'PXFTP])
getUserServers :: DB.Connection -> User -> ExceptT StoreError IO ([Maybe ServerOperator], [UserServer 'PSMP], [UserServer 'PXFTP], [UserServer 'PNTF])
getUserServers db user =
(,,)
(,,,)
<$> (map Just . serverOperators <$> getServerOperators db)
<*> liftIO (getProtocolServers db SPSMP user)
<*> liftIO (getProtocolServers db SPXFTP user)
<*> liftIO (getProtocolServers db SPNTF user)
setServerOperators :: DB.Connection -> NonEmpty ServerOperator -> IO ()
setServerOperators db ops = do
@@ -860,11 +861,12 @@ setUserServers :: DB.Connection -> User -> UTCTime -> UpdatedUserOperatorServers
setUserServers db user ts = checkConstraint SEUniqueID . liftIO . setUserServers' db user ts
setUserServers' :: DB.Connection -> User -> UTCTime -> UpdatedUserOperatorServers -> IO UserOperatorServers
setUserServers' db user@User {userId} ts UpdatedUserOperatorServers {operator, smpServers, xftpServers} = do
setUserServers' db user@User {userId} ts UpdatedUserOperatorServers {operator, smpServers, xftpServers, ntfServers} = do
mapM_ (updateServerOperator db ts) operator
smpSrvs' <- catMaybes <$> mapM (upsertOrDelete SPSMP) smpServers
xftpSrvs' <- catMaybes <$> mapM (upsertOrDelete SPXFTP) xftpServers
pure UserOperatorServers {operator, smpServers = smpSrvs', xftpServers = xftpSrvs'}
ntfSrvs' <- catMaybes <$> mapM (upsertOrDelete SPNTF) ntfServers
pure UserOperatorServers {operator, smpServers = smpSrvs', xftpServers = xftpSrvs', ntfServers = ntfSrvs'}
where
upsertOrDelete :: ProtocolTypeI p => SProtocolType p -> AUserServer p -> IO (Maybe (UserServer p))
upsertOrDelete p (AUS _ s@UserServer {serverId, deleted}) = case serverId of
+3 -1
View File
@@ -139,6 +139,7 @@ import Simplex.Chat.Store.SQLite.Migrations.M20250721_indexes
import Simplex.Chat.Store.SQLite.Migrations.M20250729_member_contact_requests
import Simplex.Chat.Store.SQLite.Migrations.M20250801_via_group_link_uri
import Simplex.Chat.Store.SQLite.Migrations.M20250802_chat_peer_type
import Simplex.Chat.Store.SQLite.Migrations.M20250815_extras
import Simplex.Messaging.Agent.Store.Shared (Migration (..))
schemaMigrations :: [(String, Query, Maybe Query)]
@@ -277,7 +278,8 @@ schemaMigrations =
("20250721_indexes", m20250721_indexes, Just down_m20250721_indexes),
("20250729_member_contact_requests", m20250729_member_contact_requests, Just down_m20250729_member_contact_requests),
("20250801_via_group_link_uri", m20250801_via_group_link_uri, Just down_m20250801_via_group_link_uri),
("20250802_chat_peer_type", m20250802_chat_peer_type, Just down_m20250802_chat_peer_type)
("20250802_chat_peer_type", m20250802_chat_peer_type, Just down_m20250802_chat_peer_type),
("20250815_extras", m20250815_extras, Just down_m20250815_extras)
]
-- | The list of migrations in ascending order by date
@@ -0,0 +1,18 @@
{-# LANGUAGE QuasiQuotes #-}
module Simplex.Chat.Store.SQLite.Migrations.M20250815_extras where
import Database.SQLite.Simple (Query)
import Database.SQLite.Simple.QQ (sql)
m20250815_extras :: Query
m20250815_extras =
[sql|
ALTER TABLE protocol_servers ADD COLUMN extras TEXT;
|]
down_m20250815_extras :: Query
down_m20250815_extras =
[sql|
ALTER TABLE protocol_servers DROP COLUMN extras;
|]
@@ -494,6 +494,7 @@ CREATE TABLE IF NOT EXISTS "protocol_servers"(
host TEXT NOT NULL,
port TEXT NOT NULL,
key_hash BLOB NOT NULL,
extras TEXT,
basic_auth TEXT,
preset INTEGER NOT NULL DEFAULT 0,
tested INTEGER,
+3 -2
View File
@@ -50,10 +50,11 @@ terminalChatConfig =
],
useSMP = 3,
xftp = map (presetServer True) $ L.toList defaultXFTPServers,
useXFTP = 3
useXFTP = 3,
ntf = map (presetServer True) $ L.toList _defaultNtfServers,
useNTF = 3
}
],
ntf = _defaultNtfServers,
netCfg =
defaultNetworkConfig
{ smpProxyMode = SPMUnknown,
+3 -2
View File
@@ -1459,11 +1459,12 @@ viewUserPrivacy User {userId} User {userId = userId', localDisplayName = n', sho
]
viewUserServers :: UserOperatorServers -> [StyledString]
viewUserServers (UserOperatorServers _ [] []) = []
viewUserServers UserOperatorServers {operator, smpServers, xftpServers} =
viewUserServers (UserOperatorServers _ [] [] []) = []
viewUserServers UserOperatorServers {operator, smpServers, xftpServers, ntfServers} =
[plain $ maybe "Your servers" shortViewOperator operator]
<> viewServers SPSMP smpServers
<> viewServers SPXFTP xftpServers
<> viewServers SPNTF ntfServers
where
viewServers :: (ProtocolTypeI p, UserProtocol p) => SProtocolType p -> [UserServer p] -> [StyledString]
viewServers _ [] = []