diff --git a/apps/multiplatform/android/build.gradle.kts b/apps/multiplatform/android/build.gradle.kts index 8168c92bf8..09489a023a 100644 --- a/apps/multiplatform/android/build.gradle.kts +++ b/apps/multiplatform/android/build.gradle.kts @@ -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") diff --git a/apps/multiplatform/android/src/main/AndroidManifest.xml b/apps/multiplatform/android/src/main/AndroidManifest.xml index d6059896a5..b226d2709f 100644 --- a/apps/multiplatform/android/src/main/AndroidManifest.xml +++ b/apps/multiplatform/android/src/main/AndroidManifest.xml @@ -186,6 +186,13 @@ android:foregroundServiceType="mediaPlayback|microphone|camera|remoteMessaging" /> + + + + + + 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) + } } diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/PushService.kt b/apps/multiplatform/android/src/main/java/chat/simplex/app/PushService.kt new file mode 100644 index 0000000000..37d3a72ad1 --- /dev/null +++ b/apps/multiplatform/android/src/main/java/chat/simplex/app/PushService.kt @@ -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" + } +} \ No newline at end of file diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexApp.kt b/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexApp.kt index 83767f90d7..7b32efbb2d 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexApp.kt +++ b/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexApp.kt @@ -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 } } diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexService.kt b/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexService.kt index 289ecc0a31..0b78d8a57e 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexService.kt +++ b/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexService.kt @@ -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() diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/model/NtfManager.android.kt b/apps/multiplatform/android/src/main/java/chat/simplex/app/model/NtfManager.android.kt index 5d8371708c..fda263063b 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/model/NtfManager.android.kt +++ b/apps/multiplatform/android/src/main/java/chat/simplex/app/model/NtfManager.android.kt @@ -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) diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/platform/PushManager.kt b/apps/multiplatform/android/src/main/java/chat/simplex/app/platform/PushManager.kt new file mode 100644 index 0000000000..7d0a573489 --- /dev/null +++ b/apps/multiplatform/android/src/main/java/chat/simplex/app/platform/PushManager.kt @@ -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.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) = 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) = 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) { + val userServersState = mutableStateOf(userServers) + val serverErrors = mutableStateOf(listOf()) + 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, 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 + } + +} \ No newline at end of file diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/helpers/Extensions.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/helpers/Extensions.kt index 540533e5ad..6ddf0c35ba 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/helpers/Extensions.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/helpers/Extensions.kt @@ -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 diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt index 1b0714dd9d..61d2734979 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt @@ -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, 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 = listOf(), val smpServers: List, val xftpServers: List ) { @@ -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 diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Platform.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Platform.kt index 448100bc17..47d9b5a1ad 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Platform.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Platform.kt @@ -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. diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/NotificationsSettingsView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/NotificationsSettingsView.kt index 2fc427cd2e..88a6dc3e4f 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/NotificationsSettingsView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/NotificationsSettingsView.kt @@ -133,6 +133,15 @@ private fun notificationModes(): List> { 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> { } 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) + } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/NetworkAndServers.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/NetworkAndServers.kt index 26ecf151ff..8a6665e760 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/NetworkAndServers.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/NetworkAndServers.kt @@ -932,6 +932,15 @@ fun globalXFTPServersError(serverErrors: List): String? { return null } +fun globalNTFServersError(serverErrors: List): String? { + for (err in serverErrors) { + if (err.globalNTFError != null) { + return err.globalNTFError + } + } + return null +} + fun findDuplicateHosts(serverErrors: List): Set { val duplicateHostsList = serverErrors.mapNotNull { err -> if (err is UserServersError.DuplicateServer) { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/NewServerView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/NewServerView.kt index 1ec2534ab1..c2f659ba0f 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/NewServerView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/NewServerView.kt @@ -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) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/OperatorView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/OperatorView.kt index cc72387875..3220f66868 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/OperatorView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/OperatorView.kt @@ -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 diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/ProtocolServerView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/ProtocolServerView.kt index 8626fd3143..fe0281c52c 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/ProtocolServerView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/ProtocolServerView.kt @@ -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) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/ProtocolServersView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/ProtocolServersView.kt index 63bf8b1dc4..0c39bd372f 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/ProtocolServersView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/ProtocolServersView.kt @@ -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, xftpServers: List, + ntfServers: List, testing: MutableState, onUpdate: (ServerProtocol, List) -> 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, smpServers: List, xftpServers: List, + ntfServers: List, m: ChatModel, onUpdate: (ServerProtocol, List) -> 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, m: ChatModel, onUp return fs } +fun deleteNTFServer( + userServers: MutableState>, + 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>, operatorServersIndex: Int, diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml index b1a9e39157..e34f75787a 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -137,6 +137,8 @@ No media & file servers. No servers to send files. No servers to receive files. + No notification servers. + No servers to notify. For chat profile %s: Errors in servers configuration. Error accepting conditions @@ -270,9 +272,11 @@ Runs when app is open Starts periodically Always on + Instant App can receive notifications only when it\'s running, no background service will be started Checks new messages every 10 minutes for up to 1 minute Background service is always running – notifications will be shown as soon as the messages are available. + Receive notifications instantly using an external push service. Message text Contact name Hidden @@ -284,6 +288,16 @@ New contact request Connected Error showing notification, contact developers. + You first need to set a NTF server to get push notifications.\n\nIt will send the notifications to your push provider. + Your NTF server doesn\'t support VAPID, which is required to get push notifications. + You don\'t have any push service installed on your device.\n\nPlease installed one and try again.\n\nFor more information, visit\ + Your NTF server has been removed or has changed and doesn\'t support VAPID, which is required to get push notifications. + Select Push Service + Multiple push services are installed on your system, please select the service you wish to use. + 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. + Registration with your UnifiedPush distributor failed due to a network issue. Please try again when your network is back. + Registration with your UnifiedPush distributor failed due to a missing requirement from your distributor. + Registration with your UnifiedPush distributor failed due to an unknown issue. This could be a missing requirement from your distributor. SimpleX Lock @@ -949,6 +963,7 @@ The servers for new connections of your current chat profile Save servers? Media & file servers + Notifications servers XFTP servers Configured XFTP servers Other XFTP servers @@ -1995,6 +2010,7 @@ Use for files To send The servers for new files of your current chat profile + The servers for your notifications. Added media & file servers Open conditions Open changes @@ -2734,6 +2750,7 @@ Starting from %s. SMP server XFTP server + NTF server Reconnect attempts Sent directly diff --git a/cabal.project b/cabal.project index 05457262b7..3eb7d516f8 100644 --- a/cabal.project +++ b/cabal.project @@ -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 diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index df4ed32f05..e4a0d027b2 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -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"; diff --git a/simplex-chat.cabal b/simplex-chat.cabal index 9b9c5a9581..64eb72943d 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -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: diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index e14275b75c..d6292ec0b0 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -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} = diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index eb7ed53283..d58ec0e7a0 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -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} diff --git a/src/Simplex/Chat/Library/Commands.hs b/src/Simplex/Chat/Library/Commands.hs index 4ae58d8a29..a181875448 100644 --- a/src/Simplex/Chat/Library/Commands.hs +++ b/src/Simplex/Chat/Library/Commands.hs @@ -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 ','), diff --git a/src/Simplex/Chat/Library/Internal.hs b/src/Simplex/Chat/Library/Internal.hs index ff60c6c32f..6421619a5a 100644 --- a/src/Simplex/Chat/Library/Internal.hs +++ b/src/Simplex/Chat/Library/Internal.hs @@ -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 diff --git a/src/Simplex/Chat/Mobile.hs b/src/Simplex/Chat/Mobile.hs index f18a3900da..6ca1be0ff3 100644 --- a/src/Simplex/Chat/Mobile.hs +++ b/src/Simplex/Chat/Mobile.hs @@ -245,6 +245,7 @@ mobileChatOpts dbOptions = { dbOptions, smpServers = [], xftpServers = [], + ntfServers = [], simpleNetCfg = defaultSimpleNetCfg, logLevel = CLLImportant, logConnections = False, diff --git a/src/Simplex/Chat/Operators.hs b/src/Simplex/Chat/Operators.hs index c9b5e020d9..683172790a 100644 --- a/src/Simplex/Chat/Operators.hs +++ b/src/Simplex/Chat/Operators.hs @@ -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} diff --git a/src/Simplex/Chat/Operators/Presets.hs b/src/Simplex/Chat/Operators/Presets.hs index d3d727ea05..3c1421733c 100644 --- a/src/Simplex/Chat/Operators/Presets.hs +++ b/src/Simplex/Chat/Operators/Presets.hs @@ -116,3 +116,6 @@ fluxXFTPServers = "xftp://Rh19D5e4Eez37DEE9hAlXDB3gZa1BdFYJTPgJWPO9OI=@xftp5.simplexonflux.com,q7itltdn32hjmgcqwhow4tay5ijetng3ur32bolssw32fvc5jrwvozad.onion", "xftp://0AznwoyfX8Od9T_acp1QeeKtxUi676IBIiQjXVwbdyU=@xftp6.simplexonflux.com,upvzf23ou6nrmaf3qgnhd6cn3d74tvivlmz3p7wdfwq6fhthjrjiiqid.onion" ] + +fluxNTFServers :: [NewUserServer 'PNTF] +fluxNTFServers = [] diff --git a/src/Simplex/Chat/Options.hs b/src/Simplex/Chat/Options.hs index 9411c66175..959437c36a 100644 --- a/src/Simplex/Chat/Options.hs +++ b/src/Simplex/Chat/Options.hs @@ -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, diff --git a/src/Simplex/Chat/Store/Profiles.hs b/src/Simplex/Chat/Store/Profiles.hs index 64fbbddda0..7f44d59d2e 100644 --- a/src/Simplex/Chat/Store/Profiles.hs +++ b/src/Simplex/Chat/Store/Profiles.hs @@ -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 diff --git a/src/Simplex/Chat/Store/SQLite/Migrations.hs b/src/Simplex/Chat/Store/SQLite/Migrations.hs index 3d5fc307ee..eddfce58c8 100644 --- a/src/Simplex/Chat/Store/SQLite/Migrations.hs +++ b/src/Simplex/Chat/Store/SQLite/Migrations.hs @@ -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 diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/M20250815_extras.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20250815_extras.hs new file mode 100644 index 0000000000..fb6233af40 --- /dev/null +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20250815_extras.hs @@ -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; +|] diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql b/src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql index 952d0541d9..4a47f1523b 100644 --- a/src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql +++ b/src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql @@ -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, diff --git a/src/Simplex/Chat/Terminal.hs b/src/Simplex/Chat/Terminal.hs index e432343839..ed50b1a14b 100644 --- a/src/Simplex/Chat/Terminal.hs +++ b/src/Simplex/Chat/Terminal.hs @@ -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, diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs index 6fef6cc204..5b9c744dda 100644 --- a/src/Simplex/Chat/View.hs +++ b/src/Simplex/Chat/View.hs @@ -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 _ [] = []