Merge branch 'master' into master-android

This commit is contained in:
Evgeny Poberezkin
2024-07-02 15:08:07 +01:00
44 changed files with 1113 additions and 183 deletions
+8 -9
View File
@@ -158,20 +158,19 @@ We are prioritizing users privacy and security - it would be impossible without
Our pledge to our users is that SimpleX protocols are and will remain open, and in public domain, - so anybody can build the future implementations of the clients and the servers. We are building SimpleX platform based on the same principles as email and web, but much more private and secure.
Your donations help us raise more funds any amount, even the price of the cup of coffee, would make a big difference for us.
Your donations help us raise more funds - any amount, even the price of the cup of coffee, would make a big difference for us.
It is possible to donate via:
- [GitHub](https://github.com/sponsors/simplex-chat) - it is commission-free for us.
- [OpenCollective](https://opencollective.com/simplex-chat) - it charges a commission, and also accepts donations in crypto-currencies.
- [GitHub](https://github.com/sponsors/simplex-chat) (commission-free) or [OpenCollective](https://opencollective.com/simplex-chat) (~10% commission).
- Bitcoin: bc1qd74rc032ek2knhhr3yjq2ajzc5enz3h4qwnxad
- Monero: 8568eeVjaJ1RQ65ZUn9PRQ8ENtqeX9VVhcCYYhnVLxhV4JtBqw42so2VEUDQZNkFfsH5sXCuV7FN8VhRQ21DkNibTZP57Qt
- Bitcoin: 1bpefFkzuRoMY3ZuBbZNZxycbg7NYPYTG
- BCH: 1bpefFkzuRoMY3ZuBbZNZxycbg7NYPYTG
- BCH: bitcoincash:qq6c8vfvxqrk6rhdysgvkhqc24sggkfsx5nqvdlqcg
- Ethereum: 0xD9ee7Db0AD0dc1Dfa7eD53290199ED06beA04692
- USDT:
- BNB Smart Chain: 0x83fd788f7241a2be61780ea9dc72d2151e6843e2
- Tron: TNnTrKLBmdy2Wn3cAQR98dAVvWhLskQGfW
- Ethereum: 0x83fd788f7241a2be61780ea9dc72d2151e6843e2
- Solana: 43tWFWDczgAcn4Rzwkpqg2mqwnQETSiTwznmCgA2tf1L
- Ethereum: 0xD9ee7Db0AD0dc1Dfa7eD53290199ED06beA04692
- Solana: 7JCf5m3TiHmYKZVr6jCu1KeZVtb9Y1jRMQDU69p5ARnu
- please ask if you want to donate any other coins.
Thank you,
@@ -14,6 +14,7 @@ struct ProtocolServerView: View {
let serverProtocol: ServerProtocol
@Binding var server: ServerCfg
@State var serverToEdit: ServerCfg
@State var serverEnabled: Bool
@State private var showTestFailure = false
@State private var testing = false
@State private var testFailure: ProtocolTestFailure?
@@ -110,7 +111,10 @@ struct ProtocolServerView: View {
Spacer()
showTestStatus(server: serverToEdit)
}
Toggle("Use for new connections", isOn: $serverToEdit.enabled)
Toggle("Use for new connections", isOn: $serverEnabled)
.onChange(of: serverEnabled) { enabled in
serverToEdit.enabled = enabled ? .enabled : .disabled
}
}
}
}
@@ -179,7 +183,8 @@ struct ProtocolServerView_Previews: PreviewProvider {
ProtocolServerView(
serverProtocol: .smp,
server: Binding.constant(ServerCfg.sampleData.custom),
serverToEdit: ServerCfg.sampleData.custom
serverToEdit: ServerCfg.sampleData.custom,
serverEnabled: true
)
}
}
@@ -159,7 +159,7 @@ struct ProtocolServersView: View {
}
private var allServersDisabled: Bool {
servers.allSatisfy { !$0.enabled }
servers.allSatisfy { $0.enabled != .enabled }
}
private func protocolServerView(_ server: Binding<ServerCfg>) -> some View {
@@ -168,7 +168,8 @@ struct ProtocolServersView: View {
ProtocolServerView(
serverProtocol: serverProtocol,
server: server,
serverToEdit: srv
serverToEdit: srv,
serverEnabled: srv.enabled == .enabled
)
.navigationBarTitle(srv.preset ? "Preset server" : "Your server")
.navigationBarTitleDisplayMode(.large)
@@ -181,7 +182,7 @@ struct ProtocolServersView: View {
invalidServer()
} else if !uniqueAddress(srv, address) {
Image(systemName: "exclamationmark.circle").foregroundColor(.red)
} else if !srv.enabled {
} else if srv.enabled != .enabled {
Image(systemName: "slash.circle").foregroundColor(.secondary)
} else {
showTestStatus(server: srv)
@@ -194,7 +195,7 @@ struct ProtocolServersView: View {
.padding(.trailing, 4)
let v = Text(address?.hostnames.first ?? srv.server).lineLimit(1)
if srv.enabled {
if srv.enabled == .enabled {
v
} else {
v.foregroundColor(.secondary)
@@ -235,7 +236,7 @@ struct ProtocolServersView: View {
private func addAllPresets() {
for srv in presetServers {
if !hasPreset(srv) {
servers.append(ServerCfg(server: srv, preset: true, tested: nil, enabled: true))
servers.append(ServerCfg(server: srv, preset: true, tested: nil, enabled: .enabled))
}
}
}
@@ -260,7 +261,7 @@ struct ProtocolServersView: View {
private func resetTestStatus() {
for i in 0..<servers.count {
if servers[i].enabled {
if servers[i].enabled == .enabled {
servers[i].tested = nil
}
}
@@ -269,7 +270,7 @@ struct ProtocolServersView: View {
private func runServersTest() async -> [String: ProtocolTestFailure] {
var fs: [String: ProtocolTestFailure] = [:]
for i in 0..<servers.count {
if servers[i].enabled {
if servers[i].enabled == .enabled {
if let f = await testServerConnection(server: $servers[i]) {
fs[serverHostname(servers[i].server)] = f
}
@@ -40,7 +40,7 @@ struct ScanProtocolServer: View {
switch resp {
case let .success(r):
if parseServerAddress(r.string) != nil {
servers.append(ServerCfg(server: r.string, preset: false, tested: nil, enabled: true))
servers.append(ServerCfg(server: r.string, preset: false, tested: nil, enabled: .enabled))
dismiss()
} else {
showAddressError = true
+12 -6
View File
@@ -1109,13 +1109,13 @@ public struct ServerCfg: Identifiable, Equatable, Codable {
public var server: String
public var preset: Bool
public var tested: Bool?
public var enabled: Bool
public var enabled: ServerEnabled
var createdAt = Date()
// public var sendEnabled: Bool // can we potentially want to prevent sending on the servers we use to receive?
// Even if we don't see the use case, it's probably better to allow it in the model
// In any case, "trusted/known" servers are out of scope of this change
public init(server: String, preset: Bool, tested: Bool?, enabled: Bool) {
public init(server: String, preset: Bool, tested: Bool?, enabled: ServerEnabled) {
self.server = server
self.preset = preset
self.tested = tested
@@ -1128,7 +1128,7 @@ public struct ServerCfg: Identifiable, Equatable, Codable {
public var id: String { "\(server) \(createdAt)" }
public static var empty = ServerCfg(server: "", preset: false, tested: nil, enabled: true)
public static var empty = ServerCfg(server: "", preset: false, tested: nil, enabled: .enabled)
public var isEmpty: Bool {
server.trimmingCharacters(in: .whitespaces) == ""
@@ -1145,19 +1145,19 @@ public struct ServerCfg: Identifiable, Equatable, Codable {
server: "smp://abcd@smp8.simplex.im",
preset: true,
tested: true,
enabled: true
enabled: .enabled
),
custom: ServerCfg(
server: "smp://abcd@smp9.simplex.im",
preset: false,
tested: false,
enabled: false
enabled: .disabled
),
untested: ServerCfg(
server: "smp://abcd@smp10.simplex.im",
preset: false,
tested: nil,
enabled: true
enabled: .enabled
)
)
@@ -1169,6 +1169,12 @@ public struct ServerCfg: Identifiable, Equatable, Codable {
}
}
public enum ServerEnabled: String, Codable {
case disabled
case enabled
case known
}
public enum ProtocolTestStep: String, Decodable, Equatable {
case connect
case disconnect
@@ -52,6 +52,7 @@ object ChatModel {
val chatDbStatus = mutableStateOf<DBMigrationResult?>(null)
val ctrlInitInProgress = mutableStateOf(false)
val dbMigrationInProgress = mutableStateOf(false)
val incompleteInitializedDbRemoved = mutableStateOf(false)
val chats = mutableStateListOf<Chat>()
// map of connections network statuses, key is agent connection id
val networkStatuses = mutableStateMapOf<String, NetworkStatus>()
@@ -176,6 +176,12 @@ class AppPreferences {
val selfDestruct = mkBoolPreference(SHARED_PREFS_SELF_DESTRUCT, false)
val selfDestructDisplayName = mkStrPreference(SHARED_PREFS_SELF_DESTRUCT_DISPLAY_NAME, null)
// This flag is set when database is first initialized and resets only when the database is removed.
// This is needed for recover from incomplete initialization when only one database file is created.
// If false - the app will clear database folder on missing file and re-initialize.
// Note that this situation can only happen if passphrase for the first database is incorrect because, otherwise, backend will re-create second database automatically
val newDatabaseInitialized = mkBoolPreference(SHARED_PREFS_NEW_DATABASE_INITIALIZED, false)
val currentTheme = mkStrPreference(SHARED_PREFS_CURRENT_THEME, DefaultTheme.SYSTEM_THEME_NAME)
val systemDarkTheme = mkStrPreference(SHARED_PREFS_SYSTEM_DARK_THEME, DefaultTheme.SIMPLEX.themeName)
val currentThemeIds = mkMapPreference(SHARED_PREFS_CURRENT_THEME_IDs, mapOf(), encode = {
@@ -361,6 +367,7 @@ class AppPreferences {
private const val SHARED_PREFS_ENCRYPTED_SELF_DESTRUCT_PASSPHRASE = "EncryptedSelfDestructPassphrase"
private const val SHARED_PREFS_INITIALIZATION_VECTOR_SELF_DESTRUCT_PASSPHRASE = "InitializationVectorSelfDestructPassphrase"
private const val SHARED_PREFS_ENCRYPTION_STARTED_AT = "EncryptionStartedAt"
private const val SHARED_PREFS_NEW_DATABASE_INITIALIZED = "NewDatabaseInitialized"
private const val SHARED_PREFS_CONFIRM_DB_UPGRADES = "ConfirmDBUpgrades"
private const val SHARED_PREFS_SELF_DESTRUCT = "LocalAuthenticationSelfDestruct"
private const val SHARED_PREFS_SELF_DESTRUCT_DISPLAY_NAME = "LocalAuthenticationSelfDestructDisplayName"
@@ -3018,7 +3025,7 @@ data class ServerCfg(
val server: String,
val preset: Boolean,
val tested: Boolean? = null,
val enabled: Boolean
val enabled: ServerEnabled
) {
@Transient
private val createdAt: Date = Date()
@@ -3032,7 +3039,7 @@ data class ServerCfg(
get() = server.isBlank()
companion object {
val empty = ServerCfg(remoteHostId = null, server = "", preset = false, tested = null, enabled = true)
val empty = ServerCfg(remoteHostId = null, server = "", preset = false, tested = null, enabled = ServerEnabled.Enabled)
class SampleData(
val preset: ServerCfg,
@@ -3046,26 +3053,33 @@ data class ServerCfg(
server = "smp://abcd@smp8.simplex.im",
preset = true,
tested = true,
enabled = true
enabled = ServerEnabled.Enabled
),
custom = ServerCfg(
remoteHostId = null,
server = "smp://abcd@smp9.simplex.im",
preset = false,
tested = false,
enabled = false
enabled = ServerEnabled.Disabled
),
untested = ServerCfg(
remoteHostId = null,
server = "smp://abcd@smp10.simplex.im",
preset = false,
tested = null,
enabled = true
enabled = ServerEnabled.Enabled
)
)
}
}
@Serializable
enum class ServerEnabled {
@SerialName("disabled") Disabled,
@SerialName("enabled") Enabled,
@SerialName("known") Known;
}
@Serializable
enum class ProtocolTestStep {
@SerialName("connect") Connect,
@@ -88,8 +88,23 @@ suspend fun initChatController(useKey: String? = null, confirmMigrations: Migrat
chatModel.chatDbStatus.value = res
if (res != DBMigrationResult.OK) {
Log.d(TAG, "Unable to migrate successfully: $res")
if (!appPrefs.newDatabaseInitialized.get() && DatabaseUtils.hasOnlyOneDatabase(dataDir.absolutePath)) {
if (chatModel.incompleteInitializedDbRemoved.value) {
Log.d(TAG, "Incomplete initialized databases were removed but after repeated migration only one database exists again, not trying to remove again")
} else {
val dbPath = dbAbsolutePrefixPath
File(dbPath + "_chat.db").delete()
File(dbPath + "_agent.db").delete()
chatModel.incompleteInitializedDbRemoved.value = true
Log.d(TAG, "Incomplete initialized databases were removed for the first time, repeating migration")
chatModel.ctrlInitInProgress.value = false
initChatController(useKey, confirmMigrations, startChat)
}
}
return
}
appPrefs.newDatabaseInitialized.set(true)
chatModel.incompleteInitializedDbRemoved.value = false
platform.androidRestartNetworkObserver()
controller.apiSetAppFilePaths(
appFilesDir.absolutePath,
@@ -22,8 +22,11 @@ import chat.simplex.common.platform.*
import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.helpers.*
import chat.simplex.common.views.usersettings.AppVersionText
import chat.simplex.common.views.usersettings.SettingsActionItem
import chat.simplex.res.MR
import dev.icerock.moko.resources.StringResource
import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
import kotlinx.coroutines.*
import kotlinx.datetime.Clock
import java.io.File
@@ -106,7 +109,7 @@ fun DatabaseErrorView(
}
}
is DBMigrationResult.ErrorMigration -> when (val err = status.migrationError) {
is MigrationError.Upgrade ->
is MigrationError.Upgrade -> {
DatabaseErrorDetails(MR.strings.database_upgrade) {
TextButton({ callRunChat(confirmMigrations = MigrationConfirmation.YesUp) }, Modifier.align(Alignment.CenterHorizontally), enabled = !progressIndicator.value) {
Text(generalGetString(MR.strings.upgrade_and_open_chat))
@@ -116,7 +119,9 @@ fun DatabaseErrorView(
MigrationsText(err.upMigrations.map { it.upName })
AppVersionText()
}
is MigrationError.Downgrade ->
OpenDatabaseDirectoryButton()
}
is MigrationError.Downgrade -> {
DatabaseErrorDetails(MR.strings.database_downgrade) {
TextButton({ callRunChat(confirmMigrations = MigrationConfirmation.YesUpDown) }, Modifier.align(Alignment.CenterHorizontally), enabled = !progressIndicator.value) {
Text(generalGetString(MR.strings.downgrade_and_open_chat))
@@ -127,29 +132,41 @@ fun DatabaseErrorView(
MigrationsText(err.downMigrations)
AppVersionText()
}
is MigrationError.Error ->
OpenDatabaseDirectoryButton()
}
is MigrationError.Error -> {
DatabaseErrorDetails(MR.strings.incompatible_database_version) {
FileNameText(status.dbFile)
Text(String.format(generalGetString(MR.strings.error_with_info), mtrErrorDescription(err.mtrError)))
}
OpenDatabaseDirectoryButton()
}
}
is DBMigrationResult.ErrorSQL ->
is DBMigrationResult.ErrorSQL -> {
DatabaseErrorDetails(MR.strings.database_error) {
FileNameText(status.dbFile)
Text(String.format(generalGetString(MR.strings.error_with_info), status.migrationSQLError))
}
is DBMigrationResult.ErrorKeychain ->
OpenDatabaseDirectoryButton()
}
is DBMigrationResult.ErrorKeychain -> {
DatabaseErrorDetails(MR.strings.keychain_error) {
Text(generalGetString(MR.strings.cannot_access_keychain))
}
is DBMigrationResult.InvalidConfirmation ->
OpenDatabaseDirectoryButton()
}
is DBMigrationResult.InvalidConfirmation -> {
DatabaseErrorDetails(MR.strings.invalid_migration_confirmation) {
// this can only happen if incorrect parameter is passed
}
is DBMigrationResult.Unknown ->
OpenDatabaseDirectoryButton()
}
is DBMigrationResult.Unknown -> {
DatabaseErrorDetails(MR.strings.database_error) {
Text(String.format(generalGetString(MR.strings.unknown_database_error_with_info), status.json))
}
OpenDatabaseDirectoryButton()
}
is DBMigrationResult.OK -> {}
null -> {}
}
@@ -294,6 +311,18 @@ private fun ColumnScope.SaveAndOpenButton(enabled: Boolean, onClick: () -> Unit)
}
}
@Composable
private fun OpenDatabaseDirectoryButton() {
if (appPlatform.isDesktop) {
Spacer(Modifier.padding(top = DEFAULT_PADDING))
SettingsActionItem(
painterResource(MR.images.ic_folder_open),
stringResource(MR.strings.open_database_folder),
::desktopOpenDatabaseDir
)
}
}
@Composable
private fun ColumnScope.OpenChatButton(enabled: Boolean, onClick: () -> Unit) {
TextButton(onClick, Modifier.align(Alignment.CenterHorizontally), enabled = enabled) {
@@ -18,6 +18,7 @@ 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.appPrefs
import chat.simplex.common.model.ChatModel.controller
import chat.simplex.common.model.ChatModel.updatingChatsMutex
import chat.simplex.common.ui.theme.*
@@ -492,6 +493,7 @@ fun deleteChatDatabaseFilesAndState() {
wallpapersDir.deleteRecursively()
wallpapersDir.mkdirs()
DatabaseUtils.ksDatabasePassword.remove()
appPrefs.newDatabaseInitialized.set(false)
controller.appPrefs.storeDBPassphrase.set(true)
controller.ctrl = null
@@ -39,22 +39,25 @@ object DatabaseUtils {
}
}
private fun hasDatabase(rootDir: String): Boolean =
File(rootDir + File.separator + chatDatabaseFileName).exists() && File(rootDir + File.separator + agentDatabaseFileName).exists()
private fun hasAtLeastOneDatabase(rootDir: String): Boolean =
File(rootDir + File.separator + chatDatabaseFileName).exists() || File(rootDir + File.separator + agentDatabaseFileName).exists()
fun hasOnlyOneDatabase(rootDir: String): Boolean =
File(rootDir + File.separator + chatDatabaseFileName).exists() != File(rootDir + File.separator + agentDatabaseFileName).exists()
fun useDatabaseKey(): String {
Log.d(TAG, "useDatabaseKey ${appPreferences.storeDBPassphrase.get()}")
var dbKey = ""
val useKeychain = appPreferences.storeDBPassphrase.get()
if (useKeychain) {
if (!hasDatabase(dataDir.absolutePath)) {
if (!hasAtLeastOneDatabase(dataDir.absolutePath)) {
dbKey = randomDatabasePassword()
ksDatabasePassword.set(dbKey)
appPreferences.initialRandomDBPassphrase.set(true)
} else {
dbKey = ksDatabasePassword.get() ?: ""
}
} else if (appPlatform.isDesktop && !hasDatabase(dataDir.absolutePath)) {
} else if (appPlatform.isDesktop && !hasAtLeastOneDatabase(dataDir.absolutePath)) {
// In case of database was deleted by hand
dbKey = randomDatabasePassword()
ksDatabasePassword.set(dbKey)
@@ -175,8 +175,10 @@ private fun UseServerSection(
Text(stringResource(MR.strings.smp_servers_test_server), color = if (valid && !testing) MaterialTheme.colors.onBackground else MaterialTheme.colors.secondary)
ShowTestStatus(server)
}
val enabled = rememberUpdatedState(server.enabled)
PreferenceToggle(stringResource(MR.strings.smp_servers_use_server_for_new_conn), enabled.value) { onUpdate(server.copy(enabled = it)) }
val enabled = rememberUpdatedState(server.enabled == ServerEnabled.Enabled)
PreferenceToggle(stringResource(MR.strings.smp_servers_use_server_for_new_conn), enabled.value) { enable ->
onUpdate(server.copy(enabled = if (enable) ServerEnabled.Enabled else ServerEnabled.Disabled))
}
SectionItemView(onDelete, disabled = testing) {
Text(stringResource(MR.strings.smp_servers_delete_server), color = if (testing) MaterialTheme.colors.secondary else MaterialTheme.colors.error)
}
@@ -34,7 +34,7 @@ fun ModalData.ProtocolServersView(m: ChatModel, rhId: Long?, serverProtocol: Ser
val currServers = remember(rhId) { mutableStateOf(servers) }
val testing = rememberSaveable(rhId) { mutableStateOf(false) }
val serversUnchanged = remember(servers) { derivedStateOf { servers == currServers.value || testing.value } }
val allServersDisabled = remember { derivedStateOf { servers.none { it.enabled } } }
val allServersDisabled = remember { derivedStateOf { servers.none { it.enabled == ServerEnabled.Enabled } } }
val saveDisabled = remember(servers) {
derivedStateOf {
servers.isEmpty() ||
@@ -250,12 +250,12 @@ private fun ProtocolServerView(serverProtocol: ServerProtocol, srv: ServerCfg, s
val address = parseServerAddress(srv.server)
when {
address == null || !address.valid || address.serverProtocol != serverProtocol || !uniqueAddress(srv, address, servers) -> InvalidServer()
!srv.enabled -> Icon(painterResource(MR.images.ic_do_not_disturb_on), null, tint = MaterialTheme.colors.secondary)
srv.enabled != ServerEnabled.Enabled -> Icon(painterResource(MR.images.ic_do_not_disturb_on), null, tint = MaterialTheme.colors.secondary)
else -> ShowTestStatus(srv)
}
Spacer(Modifier.padding(horizontal = 4.dp))
val text = address?.hostnames?.firstOrNull() ?: srv.server
if (srv.enabled) {
if (srv.enabled == ServerEnabled.Enabled) {
Text(text, color = if (disabled) MaterialTheme.colors.secondary else MaterialTheme.colors.onBackground, maxLines = 1)
} else {
Text(text, maxLines = 1, color = MaterialTheme.colors.secondary)
@@ -292,7 +292,7 @@ private fun addAllPresets(rhId: Long?, presetServers: List<String>, servers: Lis
val toAdd = ArrayList<ServerCfg>()
for (srv in presetServers) {
if (!hasPreset(srv, servers)) {
toAdd.add(ServerCfg(remoteHostId = rhId, srv, preset = true, tested = null, enabled = true))
toAdd.add(ServerCfg(remoteHostId = rhId, srv, preset = true, tested = null, enabled = ServerEnabled.Enabled))
}
}
return toAdd
@@ -319,7 +319,7 @@ private suspend fun testServers(testing: MutableState<Boolean>, servers: List<Se
private fun resetTestStatus(servers: List<ServerCfg>): List<ServerCfg> {
val copy = ArrayList(servers)
for ((index, server) in servers.withIndex()) {
if (server.enabled) {
if (server.enabled == ServerEnabled.Enabled) {
copy.removeAt(index)
copy.add(index, server.copy(tested = null))
}
@@ -331,7 +331,7 @@ private suspend fun runServersTest(servers: List<ServerCfg>, m: ChatModel, onUpd
val fs: MutableMap<String, ProtocolTestFailure> = mutableMapOf()
val updatedServers = ArrayList<ServerCfg>(servers)
for ((index, server) in servers.withIndex()) {
if (server.enabled) {
if (server.enabled == ServerEnabled.Enabled) {
interruptIfCancelled()
val (updatedServer, f) = testServerConnection(server, m)
updatedServers.removeAt(index)
@@ -7,6 +7,7 @@ import dev.icerock.moko.resources.compose.stringResource
import androidx.compose.ui.unit.dp
import chat.simplex.common.model.ServerAddress.Companion.parseServerAddress
import chat.simplex.common.model.ServerCfg
import chat.simplex.common.model.ServerEnabled
import chat.simplex.common.ui.theme.DEFAULT_PADDING
import chat.simplex.common.views.helpers.*
import chat.simplex.common.views.newchat.QRCodeScanner
@@ -25,7 +26,7 @@ fun ScanProtocolServerLayout(rhId: Long?, onNext: (ServerCfg) -> Unit) {
QRCodeScanner { text ->
val res = parseServerAddress(text)
if (res != null) {
onNext(ServerCfg(remoteHostId = rhId, text, false, null, true))
onNext(ServerCfg(remoteHostId = rhId, text, false, null, ServerEnabled.Enabled))
} else {
AlertManager.shared.showAlertMsg(
title = generalGetString(MR.strings.smp_servers_invalid_address),
+1 -1
View File
@@ -12,7 +12,7 @@ constraints: zip +disable-bzip2 +disable-zstd
source-repository-package
type: git
location: https://github.com/simplex-chat/simplexmq.git
tag: 8a3b72458f917e9867f4e3640dda0fa1827ff6cf
tag: f392ce0a9355cd3883400906ae6c361b77ca46ea
source-repository-package
type: git
+25 -30
View File
@@ -15,29 +15,32 @@ We want to add up to 3 people to the team.
## Who we are looking for
### Product/UI designer
You will be designing the user experience and the interface of both the app and the website in collaboration with the team.
The current focus of the app is privacy and security, but we hope to have the design that would support the feeling of psychological safety, enabling people to achieve the results in the smallest amount of time.
You are an experienced and innovative product designer with:
- 8+ years of user experience and visual design.
- Expertise in typography and high sensitivity to colors.
- Exceptional precision and attention to details.
- Strong opinions (weakly held).
- A strong empathy.
### Application Haskell engineer
You will work with the Haskell core of the client applications and with the network servers.
You are an expert in language models, databases and Haskell:
- expert knowledge of SQL.
- exception handling, concurrency, STM.
- type systems - we use ad hoc dependent types a lot.
- experience integrating open-source language models.
- experience developing community-centric applications.
- interested to build the next generation of messaging network.
You will be focussed mostly on our client applications, and will also contribute to the servers also written in Haskell.
### iOS / Mac engineer
You are an expert in Apple platforms, including:
- iOS and Mac platform architecture.
- Swift and Objective-C.
- SwiftUI and UIKit.
- extensions, including notification service extension and sharing extension.
- low level inter-process communication primitives for concurrency.
- interested about creating the next generation of UX for a communication/social network.
Knowledge of Android and Kotlin Multiplatform would be a bonus - we use Kotlin Jetpack Compose for our Android and desktop apps.
- Haskell exception handling, concurrency, STM, type systems.
- 8y+ of software engineering experience in complex projects,
- deep understanding of the common programming principles:
- data structures, bits and bytes, text encoding.
- software design and algorithms.
- concurrency.
- networking.
## About you
@@ -53,20 +56,10 @@ Knowledge of Android and Kotlin Multiplatform would be a bonus - we use Kotlin J
- focus on solving only today's problems and resist engineering for the future (aka over-engineering) see [The Duct Tape Programmer](https://www.joelonsoftware.com/2009/09/23/the-duct-tape-programmer/) and [Why I Hate Frameworks](https://medium.com/@johnfliu/why-i-hate-frameworks-6af8cbadba42).
- do not suffer from "not invented here" syndrome, at the same time interested to design and implement protocols and systems from the ground up when appropriate.
- **Love software engineering**:
- have 5y+ of software engineering experience in complex projects,
- great understanding of the common principles:
- data structures, bits and byte manipulation
- text encoding and manipulation
- software design and algorithms
- concurrency
- networking
- **Want to join a very early stage startup**:
- high pace and intensity, longer hours.
- a substantial part of the compensation is stock options.
- full transparency we believe that too much [autonomy](https://twitter.com/KentBeck/status/851459129830850561) hurts learning and slows down progress.
- full transparency - we believe that too much [autonomy](https://twitter.com/KentBeck/status/851459129830850561) hurts learning and slows down progress.
## How to join the team
@@ -75,3 +68,5 @@ Knowledge of Android and Kotlin Multiplatform would be a bonus - we use Kotlin J
2. Also look through [GitHub issues](https://github.com/simplex-chat/simplex-chat/issues) submitted by the users to see what would you want to contribute as a test.
3. [Connect to us](https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FKBCmxJ3-lEjpWLPPkI6OWPk-YJneU5uY%23%2F%3Fv%3D1%26dh%3DMCowBQYDK2VuAyEAtixHJWDXvYWcoe-77vIfjvI6XWEuzUsapMS9nVHP_Go%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) via SimpleX Chat to chat about what you want to contribute and about joining the team.
4. You can also email [jobs@simplex.chat](mailto:jobs@simplex.chat?subject=Join%20SimpleX%20Chat%20team)
+1 -1
View File
@@ -75,7 +75,7 @@ Manual installation requires some preliminary actions:
Group=xftp
Type=simple
ExecStart=/usr/local/bin/xftp-server start +RTS -N -RTS
ExecStopPost=/usr/bin/env sh -c '[ -e "/var/opt/simplex-xftp/file-server-store.log" ] && cp "/var/opt/simplex-xftp/file-server-store.log" "/var/opt/simplex-xftp/file-server-store.log.$(date +'%FT%T')"'
ExecStopPost=/usr/bin/env sh -c '[ -e "/var/opt/simplex-xftp/file-server-store.log" ] && cp "/var/opt/simplex-xftp/file-server-store.log" "/var/opt/simplex-xftp/file-server-store.log.$(date +'%%FT%%T')"'
LimitNOFILE=65535
KillSignal=SIGINT
TimeoutStopSec=infinity
+22 -4
View File
@@ -4,9 +4,16 @@ sequenceDiagram
participant B as Bob
participant C as Existing<br>contact
note over A, B: 1. send and accept group invitation
A ->> B: x.grp.inv<br>invite Bob to group<br>(via contact connection)
B ->> A: x.grp.acpt<br>accept invitation<br>(via member connection)<br>establish group member connection
alt invite contact
note over A, B: 1a. send and accept group invitation
A ->> B: x.grp.inv<br>invite Bob to group<br>(via contact connection)
B ->> A: x.grp.acpt<br>accept invitation<br>(via member connection)<br>establish group member connection
else join via group link
note over A, B: 1b. join via group link and accept request
B ->> A: join via group link<br>SimpleX contact address
A ->> B: x.grp.link.inv in SMP confirmation<br>accept joining member request,<br>sending group profile, etc.<br>establish group member connection
A ->> B: x.grp.link.mem<br>send inviting member profile
end
note over M, B: 2. introduce new member Bob to all existing members
A ->> M: x.grp.mem.new<br>"announce" Bob<br>to existing members<br>(via member connections)
@@ -20,14 +27,25 @@ sequenceDiagram
end
A ->> M: x.grp.mem.fwd<br>forward "invitations" and<br>Bob's chat protocol version<br>to all members<br>(via member connections)
note over M, B: group message forwarding<br>(while connections between members are being established)
M -->> B: messages between members and Bob are forwarded by Alice
B -->> M:
note over M, B: 3. establish direct and group member connections
M ->> B: establish group member connection
opt chat protocol compatible version < 2
M ->> B: establish direct connection
note over M, C: 4. deduplicate new contact
note over M, C: 3*. deduplicate new contact
B ->> M: x.info.probe<br>"probe" is sent to all new members
B ->> C: x.info.probe.check<br>"probe" hash,<br>in case contact and<br>member profiles match
C ->> B: x.info.probe.ok<br> original "probe",<br> in case contact and member<br>are the same user
note over B: merge existing and new contacts if received and sent probe hashes match
end
note over M, B: 4. notify inviting member that connection is established
M ->> A: x.grp.mem.con
B ->> A: x.grp.mem.con
note over A: stops forwarding messages
M -->> B: messages are sent via group connection without forwarding
B -->> M:
File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 41 KiB

+110 -8
View File
@@ -2,7 +2,7 @@
title: SimpleX Chat Protocol
revision: 08.08.2022
---
DRAFT Revision 0.1, 2022-08-08
Revision 2, 2024-06-24
Evgeny Poberezkin
@@ -17,18 +17,23 @@ SimpleX Chat Protocol is a protocol used by SimpleX Chat clients to exchange mes
The scope of SimpleX Chat Protocol is application level messages, both for chat functionality, related to the conversations between the clients, and extensible for any other application functions. Currently supported chat functions:
- direct and group messages,
- message replies (quoting), forwarded messages and message deletions,
- message attachments: images and files,
- message replies (quoting), message editing, forwarded messages and message deletions,
- message attachments: images, videos, voice messages and files,
- creating and managing chat groups,
- invitation and signalling for audio/video WebRTC calls.
## General message format
SimpleX Chat protocol supports two message formats:
SimpleX Chat protocol supports these message formats:
- JSON-based format for chat and application messages.
- compressed format for adapting larger messages to reduced size of message envelope, caused by addition of PQ encryption keys to SMP agent message envelope.
- binary format for sending files or any other binary data.
JSON-based message format supports batching inside a single container message, by encoding list of messages as JSON array.
Current implementation of chat protocol in SimpleX Chat uses SimpleX File Transfer Protocol (XFTP) for file transfer, with passing file description as chat protocol messages, instead passing files in binary format via SMP connections.
### JSON format for chat and application messages
This document uses JTD schemas [RFC 8927](https://www.rfc-editor.org/rfc/rfc8927.html) to define the properties of chat messages, with some additional restrictions on message properties included in metadata member of JTD schemas. In case of any contradiction between JSON examples and JTD schema the latter MUST be considered correct.
@@ -77,8 +82,22 @@ For example, this message defines a simple text message `"hello!"`:
`params` property includes message data, depending on `event`, as defined below and in [JTD schema](./simplex-chat.schema.json).
### Compressed format
The syntax of compressed message is defined by the following ABNF notation:
```abnf
compressedMessage = %s"X" 1*15780 OCTET; compressed message data
```
Compressed message is required to fit into 13388 bytes, accounting for agent overhead (see Protocol's maxCompressedMsgLength).
The actual JSON message is required to fit into 15610 bytes, accounting for group message forwarding (x.grp.msg.forward) overhead (see Protocol's maxEncodedMsgLength).
### Binary format for sending files
> Note: Planned to be deprecated. No longer used for file transfer in SimpleX Chat implementation of chat protocol.
SimpleX Chat clients use separate connections to send files using a binary format. File chunk size send in each message MUST NOT be bigger than 15,780 bytes to fit into 16kb (16384 bytes) transport block.
The syntax of each message used to send files is defined by the following ABNF notation:
@@ -117,7 +136,9 @@ SimpleX Chat Protocol supports the following message types passed in `event` pro
- `x.contact` - contact profile and additional data sent as part of contact request to a long-term contact address.
- `x.info*` - messages to send, update and de-duplicate contact profiles.
- `x.msg.*` - messages to create, update and delete content chat items.
- `x.msg.file.descr` - message to transfer XFTP file description.
- `x.file.*` - messages to accept and cancel sending files (see files sub-protocol).
- `x.direct.del` - message to notify about contact deletion.
- `x.grp.*` - messages used to manage groups and group members (see group sub-protocol).
- `x.call.*` - messages to invite to WebRTC calls and send signalling messages.
- `x.ok` - message sent during connection handshake.
@@ -136,7 +157,7 @@ This message is sent by both sides of the connection during the connection hands
### Probing for duplicate contacts
As there are no globally unique user identitifiers, when the contact a user is already connected to is added to the group by some other group member, this contact will be added to user's list of contacts as a new contact. To allow merging such contacts, "a probe" (random base64url-encoded 32 bytes) SHOULD be sent to all new members as part of `x.info.probe` message and, in case there is a contact with the same profile, the hash of the probe MAY be sent to it as part of `x.info.probe.check` message. In case both the new member and the existing contact are the same user (they would receive both the probe and its hash), the contact would send back the original probe as part of `x.info.probe.ok` message via the previously existing contact connection proving to the sender that this new member and the existing contact are the same user, in which case the sender SHOULD merge these two contacts.
As there are no globally unique user identifiers, when the contact a user is already connected to is added to the group by some other group member, this contact will be added to user's list of contacts as a new contact. To allow merging such contacts, "a probe" (random base64url-encoded 32 bytes) SHOULD be sent to all new members as part of `x.info.probe` message and, in case there is a contact with the same profile, the hash of the probe MAY be sent to it as part of `x.info.probe.check` message. In case both the new member and the existing contact are the same user (they would receive both the probe and its hash), the contact would send back the original probe as part of `x.info.probe.ok` message via the previously existing contact connection proving to the sender that this new member and the existing contact are the same user, in which case the sender SHOULD merge these two contacts.
Sending clients MAY disable this functionality, and receiving clients MAY ignore probe messages.
@@ -155,6 +176,8 @@ Message content can be one of four types:
- `text` - no file attachment is expected for this format, `text` property MUST be non-empty.
- `file` - attached file is required, `text` property MAY be empty.
- `image` - attached file is required, `text` property MAY be empty.
- `video` - attached file is required, `text` property MAY be empty.
- `voice` - attached file is required, `text` property MAY be empty.
- `link` - no file attachment is expected, `text` property MUST be non-empty. `preview` property contains information about link preview.
See `/definition/msgContent` in [JTD schema](./simplex-chat.schema.json) for message container format.
@@ -181,25 +204,29 @@ File attachment can optionally include connection address to receive the file -
`x.file.cancel` message is sent to notify the recipient that sending of the file was cancelled. It is sent in response to accepting the file with `x.file.acpt.inv` message. It is sent in the same connection where the file was offered.
`x.msg.file.descr` message is used to send XFTP file description. File descriptions that don't fit into a single chat protocol message are sent in parts, with messages including part number (`fileDescrPartNo`) and description completion marker (`fileDescrComplete`). Recipient client accumulates description parts and starts file download upon completing file description.
## Sub-protocol for chat groups
### Decentralized design for chat groups
SimpleX Chat groups are fully decentralized and do not have any globally unique group identifiers - they are only defined on client devices as a group profile and a set of bi-directional SimpleX connections with other group members. When a new member accepts group invitation, the inviting member introduces a new member to all existing members and forwards the connection addresses so that they can establish direct and group member connections.
SimpleX Chat groups are fully decentralized and do not have any globally unique group identifiers - they are only defined on client devices as a group profile and a set of bi-directional SimpleX connections with other group members. When a new member accepts group invitation or joins via group link, the inviting member introduces a new member to all existing members and forwards the connection addresses so that they can establish direct and group member connections.
There is a possibility of the attack here: as the introducing member forwards the addresses, they can substitute them with other addresses, performing MITM attack on the communication between existing and introduced members - this is similar to the communication operator being able to perform MITM on any connection between the users. To mitigate this attack this group sub-protocol will be extended to allow validating security of the connection by sending connection verification out-of-band.
Clients are RECOMMENDED to indicate in the UI whether the connection to a group member or contact was made directly or via annother user.
Clients are RECOMMENDED to indicate in the UI whether the connection to a group member or contact was made directly or via another user.
Each member in the group is identified by a group-wide unique identifier used by all members in the group. This is to allow referencing members in the messages and to allow group message integrity validation.
The diagram below shows the sequence of messages sent between the users' clients to add the new member to the group.
While introduced members establish connection inside group, inviting member forwards messages between them by sending `x.grp.msg.forward` messages. When introduced members finalize connection, they notify inviting member to stop forwarding via `x.grp.mem.con` message.
![Adding member to the group](./diagrams/group.svg)
### Member roles
Currently members can have one of three roles - `owner`, `admin` and `member`. The user that created the group is self-assigned owner role, the new members are assigned role by the member who adds them - only `owner` and `admin` members can add new members; only `owner` members can add members with `owner` role.
Currently members can have one of three roles - `owner`, `admin`, `member` and `observer`. The user that created the group is self-assigned owner role, the new members are assigned role by the member who adds them - only `owner` and `admin` members can add new members; only `owner` members can add members with `owner` role. `Observer` members only receive messages and aren't allowed to send messages.
### Messages to manage groups and add members
@@ -207,6 +234,10 @@ Currently members can have one of three roles - `owner`, `admin` and `member`. T
`x.grp.acpt` message is sent as part of group member connection handshake, only to the inviting user.
`x.grp.link.inv` message is sent as part of connection handshake to member joining via group link, and contains group profile and initial information about inviting and joining member.
`x.grp.link.mem` message is sent as part of connection handshake to member joining via group link, and contains remaining information about inviting member.
`x.grp.mem.new` message is sent by the inviting user to all connected members (and scheduled as pending to all announced but not yet connected members) to announce a new member to the existing members. This message MUST only be sent by members with `admin` or `owner` role. Receiving clients MUST ignore this message if it is received from member with `member` role.
`x.grp.mem.intro` messages are sent by the inviting user to the invited member, via their group member connection, one message for each existing member. When this message is sent by any other member than the one who invited the recipient it MUST be ignored.
@@ -219,6 +250,10 @@ Currently members can have one of three roles - `owner`, `admin` and `member`. T
`x.grp.mem.role` message is sent to update group member role - it is sent to all members by the member who updated the role of the member referenced in this message. This message MUST only be sent by members with `admin` or `owner` role. Receiving clients MUST ignore this message if it is received from member with role less than `admin`.
`x.grp.mem.restrict` message is sent to group members to communicate group member restrictions, such as member being blocked for sending messages.
`x.grp.mem.con` message is sent by members connecting inside group to inviting member, to notify the inviting member they have completed the connection and no longer require forwarding messages between them.
`x.grp.mem.del` message is sent to delete a member - it is sent to all members by the member who deletes the member referenced in this message. This message MUST only be sent by members with `admin` or `owner` role. Receiving clients MUST ignore this message if it is received from member with `member` role.
`x.grp.leave` message is sent to all members by the member leaving the group. If the only group `owner` leaves the group, it will not be possible to delete it with `x.grp.del` message - but all members can still leave the group with `x.grp.leave` message and then delete a local copy of the group.
@@ -227,6 +262,10 @@ Currently members can have one of three roles - `owner`, `admin` and `member`. T
`x.grp.info` message is sent to all members by the member who updated group profile. Only group owners can update group profiles. Clients MAY implement some conflict resolution strategy - it is currently not implemented by SimpleX Chat client. This message MUST only be sent by members with `owner` role. Receiving clients MUST ignore this message if it is received from member other than with `owner` role.
`x.grp.direct.inv` message is sent to a group member to propose establishing a direct connection between members, thus creating a contact with another member.
`x.grp.msg.forward` message is sent by inviting member to forward messages between introduced members, while they are connecting.
## Sub-protocol for WebRTC audio/video calls
This sub-protocol is used to send call invitations and to negotiate end-to-end encryption keys and pass WebRTC signalling information.
@@ -240,3 +279,66 @@ These message are used for WebRTC calls:
3. `x.call.answer`: to continue with call connection the initiating clients must reply with `x.call.answer` message. This message contains WebRTC answer and collected ICE candidates. Additional ICE candidates can be sent in `x.call.extra` message.
4. `x.call.end` message is sent to notify the other party that the call is terminated.
## Threat model
This threat model compliments SMP, XFTP, push notifications and XRCP protocols threat models:
- [SimpleX Messaging Protocol threat model](https://github.com/simplex-chat/simplexmq/blob/master/protocol/overview-tjr.md#threat-model);
- [SimpleX File Transfer Protocol threat model](https://github.com/simplex-chat/simplexmq/blob/master/protocol/xftp.md#threat-model);
- [Push notifications threat model](https://github.com/simplex-chat/simplexmq/blob/master/protocol/push-notifications.md#threat-model);
- [SimpleX Remote Control Protocol threat model](https://github.com/simplex-chat/simplexmq/blob/master/protocol/xrcp.md#threat-model).
#### A user's contact
*can:*
- send messages prohibited by user's preferences or otherwise act non-compliantly with user's preferences (for example, if message with updated preferences was lost or failed to be processed, or with modified client), in which case user client should treat such messages and actions as prohibited.
- by exchanging special messages with user's client, match user's contact with existing group members and/or contacts that have identical user profile (see [Probing for duplicate contacts](#probing-for-duplicate-contacts)).
- identify that and when a user is using SimpleX, in case user has delivery receipts enabled, or based on other automated client responses.
*cannot:*
- match user's contact with existing group members and/or contacts with different or with incognito profiles.
- match user's contact without communicating with the user's client.
#### A group member
*can:*
- send messages prohibited by group's preferences and member restrictions or otherwise act non-compliantly with preferences and restrictions (for example, if decentralized group state diverged, or with modified client), in which case user client should treat such messages and actions as prohibited.
- create a direct contact with a user if group permissions allow it.
- by exchanging special messages with user's client, match user's group member record with the existing group members and/or contacts that have identical user profile.
- undetectably send different messages to different group members, or selectively send messages to some members and not send to others.
- identify that and when a user is using SimpleX, in case user has delivery receipts enabled, or based on other automated client responses.
- join the same group several times, from the same or from different user profile, and pretend to be different members.
*cannot:*
- match user's contact with existing group members and/or contacts with different or with incognito profiles.
- match user's group member record with existing group members and/or contacts without communication of user's client.
- determine whether two group members with different or with incognito profiles are the same user.
#### A group admin
*can:*
- carry out MITM attack between user and other group member(s) when forwarding invitations for group connections (user can detect such attack by verifying connection security codes out-of-band).
- undetectably forward different messages to different group members, selectively adding, modifying, and dropping forwarded messages.
- disrupt decentralized group state by sending different messages that change group state (such as adding or removing members, member role changes, etc.) to different group members, or sending such messages selectively.
*cannot:*
- prove that two group members with incognito profiles is the same user.
+269 -12
View File
@@ -8,7 +8,7 @@
"displayName": {
"type": "string",
"metadata": {
"format": "non-empty string without spaces, the first character must not be # or @"
"format": "non-empty string, the first character must not be # or @"
}
},
"fullName": {"type": "string"}
@@ -19,6 +19,39 @@
"metadata": {
"format": "data URI format for base64 encoded image"
}
},
"contactLink": {"ref": "connReqUri"},
"preferences": {
"type": "string",
"metadata": {
"format": "JSON encoded user preferences"
}
}
},
"additionalProperties": true
},
"groupProfile": {
"properties": {
"displayName": {
"type": "string",
"metadata": {
"format": "non-empty string, the first character must not be # or @"
}
},
"fullName": {"type": "string"}
},
"optionalProperties": {
"image": {
"type": "string",
"metadata": {
"format": "data URI format for base64 encoded image"
}
},
"groupPreferences": {
"type": "string",
"metadata": {
"format": "JSON encoded user preferences"
}
}
},
"additionalProperties": true
@@ -29,6 +62,8 @@
},
"optionalProperties": {
"file": {"ref": "fileInvitation"},
"ttl": {"type": "integer"},
"live": {"type": "boolean"},
"quote": {
"properties": {
"msgRef": {"ref": "msgRef"},
@@ -56,17 +91,47 @@
}
},
"image": {
"text": {"type": "string", "metadata": {"comment": "can be empty"}},
"image": {"ref": "base64url"}
"properties": {
"text": {"type": "string", "metadata": {"comment": "can be empty"}},
"image": {"ref": "base64url"}
}
},
"video": {
"properties": {
"text": {"type": "string", "metadata": {"comment": "can be empty"}},
"image": {"ref": "base64url"},
"duration": {"type": "integer"}
}
},
"voice": {
"properties": {
"text": {"type": "string", "metadata": {"comment": "can be empty"}},
"duration": {"type": "integer"}
}
},
"file": {
"text": {"type": "string", "metadata": {"comment": "can be empty"}}
"properties": {
"text": {"type": "string", "metadata": {"comment": "can be empty"}}
}
}
},
"metadata": {
"comment": "it is RECOMMENDED that the clients support other values in `type` properties showing them as text messages in case `text` property is present"
}
},
"msgReaction" : {
"discriminator": "type",
"mapping": {
"emoji": {
"properties": {
"emoji": {
"type": "string",
"metadata": {"comment": "emoji character"}
}
}
}
}
},
"msgRef": {
"properties": {
"msgId": {"ref": "base64url"},
@@ -91,7 +156,31 @@
"fileSize": {"type": "uint32"}
},
"optionalProperties": {
"fileConnReq": {"ref": "connReqUri"}
"fileDigest": {"ref": "base64url"},
"fileConnReq": {"ref": "connReqUri"},
"fileDescr": {"ref": "fileDescription"}
}
},
"fileDescription": {
"properties": {
"fileDescrText": {
"type": "string",
"metadata": {
"format": "XFTP file description part text"
}
},
"fileDescrPartNo": {
"type": "integer",
"metadata": {
"format": "XFTP file description part number"
}
},
"fileDescrComplete": {
"type": "boolean",
"metadata": {
"format": "XFTP file description completion marker"
}
}
}
},
"linkPreview": {
@@ -100,6 +189,21 @@
"title": {"type": "string"},
"description": {"type": "string"},
"image": {"ref": "base64url"}
},
"optionalProperties": {
"content": {"ref": "linkContent"}
}
},
"linkContent": {
"discriminator": "type",
"mapping": {
"page": {},
"image": {},
"video": {
"optionalProperties": {
"duration": {"type": "integer"}
}
}
}
},
"groupInvitation": {
@@ -107,15 +211,27 @@
"fromMember": {"ref": "memberIdRole"},
"invitedMember": {"ref": "memberIdRole"},
"connRequest": {"ref": "connReqUri"},
"groupProfile": {"ref": "profile"}
"groupProfile": {"ref": "groupProfile"}
},
"optionalProperties": {
"groupLinkId": {"ref": "base64url"},
"groupSize": {"type": "integer"},
"metadata": {
"comment": "used to identify invitation via group link"
"comment": "groupLinkId is used to identify invitation via group link"
}
}
},
"groupLinkInvitation": {
"properties": {
"fromMember": {"ref": "memberIdRole"},
"fromMemberName": {"type": "string"},
"invitedMember": {"ref": "memberIdRole"},
"groupProfile": {"ref": "groupProfile"}
},
"optionalProperties": {
"groupSize": {"type": "integer"}
}
},
"memberIdRole": {
"properties": {
"memberId": {"ref": "base64url"},
@@ -127,16 +243,35 @@
"memberId": {"ref": "base64url"},
"memberRole": {"ref": "groupMemberRole"},
"profile": {"ref": "profile"}
},
"optionalProperties": {
"v": {"ref": "chatVersionRange"}
}
},
"memberRestrictions": {
"properties": {
"restriction": {"ref": "memberRestrictionStatus"}
}
},
"memberRestrictionStatus": {
"enum": ["blocked", "unrestricted"]
},
"chatVersionRange": {
"type": "string",
"metadata": {
"format": "chat version range string encoded as `<min>-<max>`, or as `<number>` if min = max"
}
},
"introInvitation": {
"properties": {
"groupConnReq": {"ref": "connReqUri"},
"groupConnReq": {"ref": "connReqUri"}
},
"optionalProperties": {
"directConnReq": {"ref": "connReqUri"}
}
},
"groupMemberRole": {
"enum": ["author", "member", "admin", "owner"]
"enum": ["observer", "author", "member", "admin", "owner"]
},
"callInvitation": {
"properties": {
@@ -257,6 +392,17 @@
"params": {"ref": "msgContainer"}
}
},
"x.msg.file.descr": {
"properties": {
"msgId": {"ref": "base64url"},
"params": {
"properties": {
"msgId": {"ref": "base64url"},
"fileDescr": {"ref": "fileDescription"}
}
}
}
},
"x.msg.update": {
"properties": {
"msgId": {"ref": "base64url"},
@@ -264,6 +410,10 @@
"properties": {
"msgId": {"ref": "base64url"},
"content": {"ref": "msgContent"}
},
"optionalProperties": {
"ttl": {"type": "integer"},
"live": {"type": "boolean"}
}
}
}
@@ -274,6 +424,24 @@
"params": {
"properties": {
"msgId": {"ref": "base64url"}
},
"optionalProperties": {
"memberId": {"ref": "base64url"}
}
}
}
},
"x.msg.react": {
"properties": {
"msgId": {"ref": "base64url"},
"params": {
"properties": {
"msgId": {"ref": "base64url"},
"reaction": {"ref": "msgReaction"},
"add": {"type": "boolean"}
},
"optionalProperties": {
"memberId": {"ref": "base64url"}
}
}
}
@@ -294,8 +462,10 @@
"params": {
"properties": {
"msgId": {"ref": "base64url"},
"fileConnReq": {"ref": "connReqUri"},
"fileName": {"type": "string"}
},
"optionalProperties": {
"fileConnReq": {"ref": "connReqUri"}
}
}
}
@@ -310,6 +480,14 @@
}
}
},
"x.direct.del": {
"properties": {
"msgId": {"ref": "base64url"},
"params": {
"properties": {}
}
}
},
"x.grp.inv": {
"properties": {
"msgId": {"ref": "base64url"},
@@ -330,6 +508,26 @@
}
}
},
"x.grp.link.inv": {
"properties": {
"msgId": {"ref": "base64url"},
"params": {
"properties": {
"groupLinkInvitation": {"ref": "groupLinkInvitation"}
}
}
}
},
"x.grp.link.mem": {
"properties": {
"msgId": {"ref": "base64url"},
"params": {
"properties": {
"profile": {"ref": "profile"}
}
}
}
},
"x.grp.mem.new": {
"properties": {
"msgId": {"ref": "base64url"},
@@ -346,6 +544,9 @@
"params": {
"properties": {
"memberInfo": {"ref": "memberInfo"}
},
"optionalProperties": {
"memberRestrictions": {"ref": "memberRestrictions"}
}
}
}
@@ -394,6 +595,27 @@
}
}
},
"x.grp.mem.restrict": {
"properties": {
"msgId": {"ref": "base64url"},
"params": {
"properties": {
"memberId": {"ref": "base64url"},
"memberRestrictions": {"ref": "memberRestrictions"}
}
}
}
},
"x.grp.mem.con": {
"properties": {
"msgId": {"ref": "base64url"},
"params": {
"properties": {
"memberId": {"ref": "base64url"}
}
}
}
},
"x.grp.mem.del": {
"properties": {
"msgId": {"ref": "base64url"},
@@ -425,7 +647,42 @@
"msgId": {"ref": "base64url"},
"params": {
"properties": {
"groupProfile": {"ref": "profile"}
"groupProfile": {"ref": "groupProfile"}
}
}
}
},
"x.grp.direct.inv": {
"properties": {
"msgId": {"ref": "base64url"},
"params": {
"properties": {
"connReq": {"ref": "connReqUri"}
},
"optionalProperties": {
"content": {"ref": "msgContent"}
}
}
}
},
"x.grp.msg.forward": {
"properties": {
"msgId": {"ref": "base64url"},
"params": {
"properties": {
"memberId": {"ref": "base64url"},
"msg": {
"type": "string",
"metadata": {
"format": "JSON encoded chat message"
}
},
"msgTs": {
"type": "string",
"metadata": {
"format": "ISO8601 UTC time of the message"
}
}
}
}
}
@@ -436,7 +693,7 @@
"params": {
"properties": {
"callId": {"ref": "base64url"},
"invitation": {}
"invitation": {"ref": "callInvitation"}
}
}
}
@@ -0,0 +1,87 @@
# Agent stats persistence
## Problem
State/state tracked in agent are lost on app restart, which makes it difficult to debug user bugs.
## Solution
Persist stats between sessions.
App terminal signals may vary per platform / be absent (?) -> persist stats periodically.
Stats would have `<userId, server>` key, so we don't want to store them in a plaintext file to not leak used servers locally -> persist in encrypted db.
There's couple of orthogonal design decision to be made:
- persist in chat or in agent db
- pros for chat:
- possibly less contention for db than agent
- pros for agent:
- no unnecessary back and forth, especially if agent starts accumulating from past sessions and has to be parameterized with past stats (see below)
- agent to start accumulating from past sessions stats, or keep past separately and only accumulate for current session from zeros
- pros for accumulating from past sessions:
- easier to maintain stats - e.g. user deletion has to remove keys, which is more convoluted if past stats are not stored in memory
- simpler UI - overall stats, no differentiation for past/current session (or less logic in backend preparing presentation data)
- pros for accumulating from zeros:
- simpler start logic - no need to restore stats from agent db / pass initial stats from chat db
- can differentiate between past sessions and current session stats in UI
### Option 1 - Persist in chat db, agent to track only current session
- Chat stores stats in such table:
```sql
CREATE TABLE agent_stats(
agent_stats_id INTEGER PRIMARY KEY, -- dummy id, there will only be one record
past_stats TEXT, -- accumulated from previous sessions
session_stats TEXT, -- current session
past_started_at TEXT NOT NULL DEFAULT(datetime('now')), -- starting point of tracking stats, reset on stats reset
session_started_at TEXT NOT NULL DEFAULT(datetime('now')), -- starting point of current session
session_updated_at TEXT NOT NULL DEFAULT(datetime('now')) -- last update of current session stats (periodic, frequent updates)
);
```
- Chat periodically calls getAgentServersStats api and updates `session_stats`.
- interval? should be short to not lose too much data, 5-30 seconds?
- On start `session_stats` are accumulated into `past_stats` and set to null.
- On user deletion, agent updates current session stats in memory (removes keys), chat has to do same for both stats fields in db.
- other cases where stats have to be manipulated in similar way?
### Option 2 - Persist in chat db, agent to accumulate stats from past sessions
- Table is only used for persistence of overall stats:
```sql
CREATE TABLE agent_stats(
agent_stats_id INTEGER PRIMARY KEY, -- dummy id, there will only be one record
agent_stats TEXT, -- overall stats - past and session
started_tracking_at TEXT NOT NULL DEFAULT(datetime('now')), -- starting point of tracking stats, reset on stats reset
updated_at TEXT NOT NULL DEFAULT(datetime('now'))
);
```
- Chat to parameterize creation of agent client with initial stats.
### Option 3 - Persist in agent db, agent to differentiate past stats and session stats
- Table in agent db similar to option 1.
- Agent is responsible for periodic updates in session, as well as accumulating into "past" and resetting session stats on start.
- Agent only communicates stats to chat on request.
- On user deletion agent is fully responsible for maintaining both in-memory session stats, and updating db records.
### Option 4 - Persist in agent db, agent to accumulate stats from past sessions
- Table in agent db similar to option 2.
- On start agent restores initial stats into memory by itself.
- Since all stats are in memory, on user deletion it's enough to update in memory without updating db.
- there is a race possible where agent crashes after updating stats (removing user keys) in memory before database stats have been overwritten by a periodic update, so it may be better to immediately overwrite and not wait for periodic update.
- still at least there's at least no additional logic to update past stats.
### Other considerations
Why is it important to timely remove user keys from past stats?
- stats not being saved for past users:
- important both privacy-wise and to not cause confusion when showing "All" stats (e.g. user summing up across users stats would have smaller total than total stats).
- to avoid accidentally mixing up with newer users.
- though we do have an AUTOINCREMENT user_id in agent so probably it wouldn't be a problem.
- on the other hand maybe we don't want to "forget" stats on user deletion so that stats would reflect networking more accurately?
+1 -1
View File
@@ -1,5 +1,5 @@
name: simplex-chat
version: 5.8.1.0
version: 5.8.2.0
#synopsis:
#description:
homepage: https://github.com/simplex-chat/simplex-chat#readme
@@ -0,0 +1,10 @@
[Desktop Entry]
Type=Application
Terminal=false
Name=SimpleX Chat
Comment=A private & encrypted open-source messenger without any user IDs (not even random ones)!
Keywords=chat;message;private;secure;simplex;
Categories=Utility;Chat;InstantMessaging;
Exec=simplex %U
Icon=chat.simplex.simplex
StartupWMClass=simplex-chat
@@ -0,0 +1,81 @@
<?xml version="1.0" encoding="UTF-8"?>
<component type="desktop-application">
<id>chat.simplex.simplex</id>
<name>SimpleX Chat</name>
<summary>A private &amp; encrypted open-source messenger without any user IDs (not even random)!</summary>
<developer id="chat.simplex">
<name>SimpleX Chat</name>
</developer>
<metadata_license>MIT</metadata_license>
<project_license>AGPL-3.0-or-later</project_license>
<description>
<p>SimpleX - the first messaging platform that has no user identifiers, not even random numbers!</p>
<p>Security assessment was done by Trail of Bits in November 2022.</p>
<p>SimpleX Chat features:</p>
<ol>
<li>end-to-end encrypted messages, with editing, replies and deletion of messages.</li>
<li>sending end-to-end encrypted images and files.</li>
<li>single-use and long-term user addresses.</li>
<li>secret chat groups - only group members know it exists and who is the member.</li>
<li>end-to-end encrypted audio and video calls.</li>
<li>private instant notifications.</li>
<li>portable chat profile - you can transfer your chat contacts and history to another device (terminal or mobile).</li>
</ol>
<p>SimpleX Chat advantages:</p>
<ol>
<li><em>Full privacy of your identity, profile, contacts and metadata:</em> unlike any other existing messaging platform, SimpleX uses no phone numbers or any other identifiers assigned to the users - not even random numbers. This protects the privacy of who you are communicating with, hiding it from SimpleX platform servers and from any observers.</li>
<li><em>Complete protection against spam and abuse:</em> as you have no identifier on SimpleX platform, you cannot be contacted unless you share a one-time invitation link or an optional temporary user address.</li>
<li><em>Full ownership, control and security of your data:</em> SimpleX stores all user data on client devices, the messages are only held temporarily on SimpleX relay servers until they are received.</li>
<li><em>Decentralized network:</em> you can use SimpleX with your own servers and still communicate with people using the servers that are pre-configured in the apps or any other SimpleX servers.</li>
</ol>
<p>You can connect to anybody you know via link or scan QR code (in the video call or in person) and start sending messages instantly - no emails, phone numbers or passwords needed.</p>
<p>Your profile and contacts are only stored in the app on your device - our servers do not have access to this information.</p>
<p>All messages are end-to-end encrypted using open-source double-ratchet protocol; the messages are routed via our servers using open-source SimpleX Messaging Protocol.</p>
</description>
<releases>
<release version="5.8.1" date="2024-06-19">
<description>
<p>General:</p>
<ol>
<li>fixes in sending/receiving files.</li>
<li>better error reporting when connecting to desktop app.</li>
<li>prevent forwarding to conversations where conversation preferences do not allow message.</li>
</ol>
<p>Android and desktop apps:</p>
<ol>
<li>support transparent theme colors for chat message bubbles.</li>
<li>do not reset changed network settings when switching SOCKS proxy on/off</li>
<li>fix swipe to reply when animation is disabled.</li>
<li>fix bug when duplicate group shown in the UI.</li>
</ol>
</description>
</release>
</releases>
<content_rating type="oars-1.1" />
<url type="homepage">https://simplex.chat/</url>
<url type="bugtracker">https://github.com/simplex-chat/simplex-chat/issues</url>
<url type="donation">https://opencollective.com/simplex-chat</url>
<url type="translate">https://simplex.chat/docs/translations</url>
<url type="faq">https://simplex.chat/faq</url>
<url type="vcs-browser">https://github.com/simplex-chat/simplex-chat</url>
<launchable type="desktop-id">chat.simplex.simplex.desktop</launchable>
<branding>
<color type="primary" scheme_preference="light">#a5f0ff</color>
<color type="primary" scheme_preference="dark">#110e26</color>
</branding>
<screenshots>
<screenshot type="default">
<image>https://simplex.chat/blog/images/simplex-desktop-light.png</image>
</screenshot>
</screenshots>
</component>
+1 -1
View File
@@ -1,5 +1,5 @@
{
"https://github.com/simplex-chat/simplexmq.git"."8a3b72458f917e9867f4e3640dda0fa1827ff6cf" = "1mmxdaj563kjmlkacxdnq62n6mzw9khampzaqghnk6iiwzdig0qy";
"https://github.com/simplex-chat/simplexmq.git"."f392ce0a9355cd3883400906ae6c361b77ca46ea" = "0id9mg30kmhlfcpnn2np3f0a4bb4smdzvhrbw6km8vv26si1js60";
"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";
+2 -1
View File
@@ -5,7 +5,7 @@ cabal-version: 1.12
-- see: https://github.com/sol/hpack
name: simplex-chat
version: 5.8.1.0
version: 5.8.2.0
category: Web, System, Services, Cryptography
homepage: https://github.com/simplex-chat/simplex-chat#readme
author: simplex.chat
@@ -160,6 +160,7 @@ library
Simplex.Chat.Remote.RevHTTP
Simplex.Chat.Remote.Transport
Simplex.Chat.Remote.Types
Simplex.Chat.Stats
Simplex.Chat.Store
Simplex.Chat.Store.AppSettings
Simplex.Chat.Store.Connections
+27 -25
View File
@@ -47,7 +47,6 @@ import qualified Data.List.NonEmpty as L
import Data.Map.Strict (Map)
import qualified Data.Map.Strict as M
import Data.Maybe (catMaybes, fromMaybe, isJust, isNothing, listToMaybe, mapMaybe, maybeToList)
import Data.Ord (Down (..))
import qualified Data.Set as S
import Data.Text (Text)
import qualified Data.Text as T
@@ -71,6 +70,7 @@ import Simplex.Chat.ProfileGenerator (generateRandomProfile)
import Simplex.Chat.Protocol
import Simplex.Chat.Remote
import Simplex.Chat.Remote.Types
import Simplex.Chat.Stats
import Simplex.Chat.Store
import Simplex.Chat.Store.AppSettings
import Simplex.Chat.Store.Connections
@@ -84,7 +84,6 @@ import Simplex.Chat.Store.Shared
import Simplex.Chat.Types
import Simplex.Chat.Types.Preferences
import Simplex.Chat.Types.Shared
import Simplex.Chat.Types.Util
import Simplex.Chat.Util (encryptFile, liftIOEither, shuffle)
import qualified Simplex.Chat.Util as U
import Simplex.FileTransfer.Client.Main (maxFileSize, maxFileSizeHard)
@@ -95,7 +94,7 @@ import Simplex.FileTransfer.Protocol (FileParty (..), FilePartyI)
import qualified Simplex.FileTransfer.Transport as XFTP
import Simplex.FileTransfer.Types (FileErrorType (..), RcvFileId, SndFileId)
import Simplex.Messaging.Agent as Agent
import Simplex.Messaging.Agent.Client (AgentStatsKey (..), SubInfo (..), agentClientStore, getAgentQueuesInfo, getAgentWorkersDetails, getAgentWorkersSummary, getNetworkConfig', ipAddressProtected, withLockMap)
import Simplex.Messaging.Agent.Client (SubInfo (..), agentClientStore, getAgentQueuesInfo, getAgentWorkersDetails, getAgentWorkersSummary, getNetworkConfig', ipAddressProtected, withLockMap)
import Simplex.Messaging.Agent.Env.SQLite (AgentConfig (..), InitialAgentServers (..), createAgentStore, defaultAgentConfig)
import Simplex.Messaging.Agent.Lock (withLock)
import Simplex.Messaging.Agent.Protocol
@@ -113,7 +112,7 @@ import qualified Simplex.Messaging.Crypto.Ratchet as CR
import Simplex.Messaging.Encoding
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Parsers (base64P)
import Simplex.Messaging.Protocol (AProtoServerWithAuth (..), AProtocolType (..), EntityId, ErrorType (..), MsgBody, MsgFlags (..), NtfServer, ProtoServerWithAuth (..), ProtocolTypeI, SProtocolType (..), SubscriptionMode (..), UserProtocol, XFTPServer, userProtocol)
import Simplex.Messaging.Protocol (AProtoServerWithAuth (..), AProtocolType (..), EntityId, ErrorType (..), MsgBody, MsgFlags (..), NtfServer, ProtoServerWithAuth (..), ProtocolServer, ProtocolTypeI, SProtocolType (..), SubscriptionMode (..), UserProtocol, XFTPServer, userProtocol)
import qualified Simplex.Messaging.Protocol as SMP
import Simplex.Messaging.ServiceScheme (ServiceScheme (..))
import qualified Simplex.Messaging.TMap as TM
@@ -176,12 +175,11 @@ defaultChatConfig =
_defaultSMPServers :: NonEmpty SMPServerWithAuth
_defaultSMPServers =
L.fromList
[ "smp://0YuTwO05YJWS8rkjn9eLJDjQhFKvIYd8d4xG8X1blIU=@smp8.simplex.im,beccx4yfxxbvyhqypaavemqurytl6hozr47wfc7uuecacjqdvwpw2xid.onion",
"smp://SkIkI6EPd2D63F4xFKfHk7I1UGZVNn6k1QWZ5rcyr6w=@smp9.simplex.im,jssqzccmrcws6bhmn77vgmhfjmhwlyr3u7puw4erkyoosywgl67slqqd.onion",
"smp://6iIcWT_dF2zN_w5xzZEY7HI2Prbh3ldP07YTyDexPjE=@smp10.simplex.im,rb2pbttocvnbrngnwziclp2f4ckjq65kebafws6g4hy22cdaiv5dwjqd.onion",
"smp://1OwYGt-yqOfe2IyVHhxz3ohqo3aCCMjtB-8wn4X_aoY=@smp11.simplex.im,6ioorbm6i3yxmuoezrhjk6f6qgkc4syabh7m3so74xunb5nzr4pwgfqd.onion",
"smp://UkMFNAXLXeAAe0beCa4w6X_zp18PwxSaSjY17BKUGXQ=@smp12.simplex.im,ie42b5weq7zdkghocs3mgxdjeuycheeqqmksntj57rmejagmg4eor5yd.onion",
"smp://enEkec4hlR3UtKx2NMpOUK_K4ZuDxjWBO1d9Y4YXVaA=@smp14.simplex.im,aspkyu2sopsnizbyfabtsicikr2s4r3ti35jogbcekhm3fsoeyjvgrid.onion"
[ "smp://h--vW7ZSkXPeOUpfxlFGgauQmXNFOzGoizak7Ult7cw=@smp15.simplex.im,oauu4bgijybyhczbnxtlggo6hiubahmeutaqineuyy23aojpih3dajad.onion",
"smp://hejn2gVIqNU6xjtGM3OwQeuk8ZEbDXVJXAlnSBJBWUA=@smp16.simplex.im,p3ktngodzi6qrf7w64mmde3syuzrv57y55hxabqcq3l5p6oi7yzze6qd.onion",
"smp://ZKe4uxF4Z_aLJJOEsC-Y6hSkXgQS5-oc442JQGkyP8M=@smp17.simplex.im,ogtwfxyi3h2h5weftjjpjmxclhb5ugufa5rcyrmg7j4xlch7qsr5nuqd.onion",
"smp://PtsqghzQKU83kYTlQ1VKg996dW4Cw4x_bvpKmiv8uns=@smp18.simplex.im,lyqpnwbs2zqfr45jqkncwpywpbtq7jrhxnib5qddtr6npjyezuwd3nqd.onion",
"smp://N_McQS3F9TGoh4ER0QstUf55kGnNSd-wXfNPZ7HukcM=@smp19.simplex.im,i53bbtoqhlc365k6kxzwdp5w3cdt433s7bwh3y32rcbml2vztiyyz5id.onion"
]
_defaultNtfServers :: [NtfServer]
@@ -369,7 +367,7 @@ activeAgentServers ChatConfig {defaultServers} p =
fromMaybe (cfgServers p defaultServers)
. nonEmpty
. map (\ServerCfg {server} -> server)
. filter (\ServerCfg {enabled} -> enabled)
. filter (\ServerCfg {enabled} -> enabled == SEEnabled)
cfgServers :: UserProtocol p => SProtocolType p -> (DefaultAgentServers -> NonEmpty (ProtoServerWithAuth p))
cfgServers p DefaultAgentServers {smp, xftp} = case p of
@@ -1315,7 +1313,7 @@ processChatCommand' vr = \case
servers' = fromMaybe (L.map toServerCfg defServers) $ nonEmpty servers
pure $ CRUserProtoServers user $ AUPS $ UserProtoServers p servers' defServers
where
toServerCfg server = ServerCfg {server, preset = True, tested = Nothing, enabled = True}
toServerCfg server = ServerCfg {server, preset = True, tested = Nothing, enabled = SEEnabled}
GetUserProtoServers aProtocol -> withUser $ \User {userId} ->
processChatCommand $ APIGetUserProtoServers userId aProtocol
APISetUserProtoServers userId (APSC p (ProtoServersConfig servers)) -> withUserId userId $ \user -> withServerProtocol p $ do
@@ -2253,17 +2251,23 @@ processChatCommand' vr = \case
CLUserContact ucId -> "UserContact " <> show ucId
CLFile fId -> "File " <> show fId
DebugEvent event -> toView event >> ok_
GetAgentServersSummary userId -> withUserId userId $ \user -> do
agentServersSummary <- lift $ withAgent' getAgentServersSummary
users <- withStore' getUsers
smpServers <- getUserServers user SPSMP
xftpServers <- getUserServers user SPXFTP
let presentedServersSummary = toPresentedServersSummary agentServersSummary users user smpServers xftpServers
pure $ CRAgentServersSummary user presentedServersSummary
where
getUserServers :: forall p. (ProtocolTypeI p, UserProtocol p) => User -> SProtocolType p -> CM [ProtocolServer p]
getUserServers users protocol = do
ChatConfig {defaultServers} <- asks config
let defServers = cfgServers protocol defaultServers
servers <- map (\ServerCfg {server} -> server) <$> withStore' (`getProtocolServers` users)
let srvs = if null servers then L.toList defServers else servers
pure $ map protoServer srvs
GetAgentWorkers -> lift $ CRAgentWorkersSummary <$> withAgent' getAgentWorkersSummary
GetAgentWorkersDetails -> lift $ CRAgentWorkersDetails <$> withAgent' getAgentWorkersDetails
GetAgentStats -> lift $ CRAgentStats . map stat <$> withAgent' getAgentStats
where
stat (AgentStatsKey {host, clientTs, cmd, res}, count) =
map B.unpack [host, clientTs, cmd, res, bshow count]
ResetAgentStats -> lift (withAgent' resetAgentStats) >> ok_
GetAgentMsgCounts -> lift $ do
counts <- map (first decodeLatin1) <$> withAgent' getMsgCounts
let allMsgs = foldl' (\(ts, ds) (_, (t, d)) -> (ts + t, ds + d)) (0, 0) counts
pure CRAgentMsgCounts {msgCounts = ("all", allMsgs) : sortOn (Down . snd) (filter (\(_, (_, d)) -> d /= 0) counts)}
GetAgentSubs -> lift $ summary <$> withAgent' getAgentSubscriptions
where
summary SubscriptionsInfo {activeSubscriptions, pendingSubscriptions, removedSubscriptions} =
@@ -7611,13 +7615,11 @@ chatCommandP =
("/version" <|> "/v") $> ShowVersion,
"/debug locks" $> DebugLocks,
"/debug event " *> (DebugEvent <$> jsonP),
"/get stats" $> GetAgentStats,
"/reset stats" $> ResetAgentStats,
"/get servers summary " *> (GetAgentServersSummary <$> A.decimal),
"/get subs" $> GetAgentSubs,
"/get subs details" $> GetAgentSubsDetails,
"/get workers" $> GetAgentWorkers,
"/get workers details" $> GetAgentWorkersDetails,
"/get msgs" $> GetAgentMsgCounts,
"/get queues" $> GetAgentQueuesInfo,
"//" *> (CustomChatCommand <$> A.takeByteString)
]
@@ -7755,7 +7757,7 @@ chatCommandP =
(Just <$> (AutoAccept <$> (" incognito=" *> onOffP <|> pure False) <*> optional (A.space *> msgContentP)))
(pure Nothing)
srvCfgP = strP >>= \case AProtocolType p -> APSC p <$> (A.space *> jsonP)
toServerCfg server = ServerCfg {server, preset = False, tested = Nothing, enabled = True}
toServerCfg server = ServerCfg {server, preset = False, tested = Nothing, enabled = SEEnabled}
rcCtrlAddressP = RCCtrlAddress <$> ("addr=" *> strP) <*> (" iface=" *> (jsonP <|> text1P))
text1P = safeDecodeUtf8 <$> A.takeTill (== ' ')
char_ = optional . A.char
+1 -1
View File
@@ -21,10 +21,10 @@ import Data.Time.Clock (UTCTime)
import Database.SQLite.Simple.FromField (FromField (..))
import Database.SQLite.Simple.ToField (ToField (..))
import Simplex.Chat.Types (Contact, ContactId, User)
import Simplex.Chat.Types.Util (decodeJSON, encodeJSON)
import qualified Simplex.Messaging.Crypto as C
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON, fromTextField_, fstToLower, singleFieldJSON)
import Simplex.Messaging.Util (decodeJSON, encodeJSON)
data Call = Call
{ contactId :: ContactId,
+4 -6
View File
@@ -60,6 +60,7 @@ import Simplex.Chat.Messages.CIContent
import Simplex.Chat.Protocol
import Simplex.Chat.Remote.AppVersion
import Simplex.Chat.Remote.Types
import Simplex.Chat.Stats (PresentedServersSummary)
import Simplex.Chat.Store (AutoAccept, ChatLockEntity, StoreError (..), UserContactLink, UserMsgReceiptSettings)
import Simplex.Chat.Types
import Simplex.Chat.Types.Preferences
@@ -75,7 +76,7 @@ import Simplex.Messaging.Agent.Protocol
import Simplex.Messaging.Agent.Store.SQLite (MigrationConfirmation, SQLiteStore, UpMigration, withTransaction)
import Simplex.Messaging.Agent.Store.SQLite.DB (SlowQueryStats (..))
import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB
import Simplex.Messaging.Client (SMPProxyMode (..), SMPProxyFallback (..))
import Simplex.Messaging.Client (SMPProxyFallback (..), SMPProxyMode (..))
import qualified Simplex.Messaging.Crypto as C
import Simplex.Messaging.Crypto.File (CryptoFile (..))
import qualified Simplex.Messaging.Crypto.File as CF
@@ -505,13 +506,11 @@ data ChatCommand
| ShowVersion
| DebugLocks
| DebugEvent ChatResponse
| GetAgentStats
| ResetAgentStats
| GetAgentServersSummary UserId
| GetAgentSubs
| GetAgentSubsDetails
| GetAgentWorkers
| GetAgentWorkersDetails
| GetAgentMsgCounts
| GetAgentQueuesInfo
| -- The parser will return this command for strings that start from "//".
-- This command should be processed in preCmdHook
@@ -756,12 +755,11 @@ data ChatResponse
| CRSQLResult {rows :: [Text]}
| CRSlowSQLQueries {chatQueries :: [SlowSQLQuery], agentQueries :: [SlowSQLQuery]}
| CRDebugLocks {chatLockName :: Maybe String, chatEntityLocks :: Map String String, agentLocks :: AgentLocks}
| CRAgentStats {agentStats :: [[String]]}
| CRAgentServersSummary {user :: User, serversSummary :: PresentedServersSummary}
| CRAgentWorkersDetails {agentWorkersDetails :: AgentWorkersDetails}
| CRAgentWorkersSummary {agentWorkersSummary :: AgentWorkersSummary}
| CRAgentSubs {activeSubs :: Map Text Int, pendingSubs :: Map Text Int, removedSubs :: Map Text [String]}
| CRAgentSubsDetails {agentSubs :: SubscriptionsInfo}
| CRAgentMsgCounts {msgCounts :: [(Text, (Int, Int))]}
| CRAgentQueuesInfo {agentQueuesInfo :: AgentQueuesInfo}
| CRContactDisabled {user :: User, contact :: Contact}
| CRConnectionDisabled {connectionEntity :: ConnectionEntity}
+8 -5
View File
@@ -29,13 +29,12 @@ import Data.Text (Text)
import qualified Data.Text as T
import Data.Text.Encoding (encodeUtf8)
import Simplex.Chat.Types
import Simplex.Chat.Types.Util
import Simplex.Messaging.Agent.Protocol (AConnectionRequestUri (..), ConnReqUriData (..), ConnectionRequestUri (..), SMPQueue (..))
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON, fstToLower, sumTypeJSON)
import Simplex.Messaging.Protocol (ProtocolServer (..))
import Simplex.Messaging.ServiceScheme (ServiceScheme (..))
import Simplex.Messaging.Util (safeDecodeUtf8)
import Simplex.Messaging.Util (decodeJSON, safeDecodeUtf8)
import System.Console.ANSI.Types
import qualified Text.Email.Validate as Email
@@ -146,7 +145,7 @@ parseMarkdown s = fromRight (unmarked s) $ A.parseOnly (markdownP <* A.endOfInpu
isSimplexLink :: Format -> Bool
isSimplexLink = \case
SimplexLink {} -> True;
SimplexLink {} -> True
_ -> False
markdownP :: Parser Markdown
@@ -223,11 +222,15 @@ markdownP = mconcat <$> A.many' fragmentP
wordMD s
| T.null s = unmarked s
| isUri s =
let t = T.takeWhileEnd isPunctuation s
uri = uriMarkdown $ T.dropWhileEnd isPunctuation s
let t = T.takeWhileEnd isPunctuation' s
uri = uriMarkdown $ T.dropWhileEnd isPunctuation' s
in if T.null t then uri else uri :|: unmarked t
| isEmail s = markdown Email s
| otherwise = unmarked s
isPunctuation' = \case
'/' -> False
')' -> False
c -> isPunctuation c
uriMarkdown s = case strDecode $ encodeUtf8 s of
Right cReq -> markdown (simplexUriFormat cReq) s
_ -> markdown Uri s
+2 -3
View File
@@ -29,12 +29,11 @@ import Simplex.Chat.Protocol
import Simplex.Chat.Types
import Simplex.Chat.Types.Preferences
import Simplex.Chat.Types.Shared
import Simplex.Chat.Types.Util
import Simplex.Messaging.Agent.Protocol (MsgErrorType (..), RatchetSyncState (..), SwitchPhase (..))
import Simplex.Messaging.Crypto.Ratchet (PQEncryption, pattern PQEncOn, pattern PQEncOff)
import Simplex.Messaging.Crypto.Ratchet (PQEncryption, pattern PQEncOff, pattern PQEncOn)
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON, fstToLower, singleFieldJSON, sumTypeJSON)
import Simplex.Messaging.Util (safeDecodeUtf8, tshow, (<$?>))
import Simplex.Messaging.Util (encodeJSON, safeDecodeUtf8, tshow, (<$?>))
data MsgDirection = MDRcv | MDSnd
deriving (Eq, Show)
+1 -2
View File
@@ -46,14 +46,13 @@ import Database.SQLite.Simple.ToField (ToField (..))
import Simplex.Chat.Call
import Simplex.Chat.Types
import Simplex.Chat.Types.Shared
import Simplex.Chat.Types.Util
import Simplex.Messaging.Agent.Protocol (VersionSMPA, pqdrSMPAgentVersion)
import Simplex.Messaging.Compression (Compressed, compress1, decompress1)
import Simplex.Messaging.Encoding
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, fromTextField_, fstToLower, parseAll, sumTypeJSON, taggedObjectJSON)
import Simplex.Messaging.Protocol (MsgBody)
import Simplex.Messaging.Util (eitherToMaybe, safeDecodeUtf8, (<$?>))
import Simplex.Messaging.Util (decodeJSON, eitherToMaybe, encodeJSON, safeDecodeUtf8, (<$?>))
import Simplex.Messaging.Version hiding (version)
-- Chat version history:
+268
View File
@@ -0,0 +1,268 @@
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE TemplateHaskell #-}
{-# OPTIONS_GHC -fno-warn-ambiguous-fields #-}
module Simplex.Chat.Stats where
import Control.Applicative ((<|>))
import qualified Data.Aeson.TH as J
import Data.Map.Strict (Map)
import qualified Data.Map.Strict as M
import Data.Maybe (fromMaybe, isJust)
import Data.Time.Clock (UTCTime)
import Simplex.Chat.Types
import Simplex.Messaging.Agent.Client
import Simplex.Messaging.Agent.Protocol (UserId)
import Simplex.Messaging.Agent.Stats
import Simplex.Messaging.Parsers (defaultJSON)
import Simplex.Messaging.Protocol
data PresentedServersSummary = PresentedServersSummary
{ statsStartedAt :: UTCTime,
currentUserServers :: ServersSummary,
allUsersServers :: ServersSummary
}
deriving (Show)
-- Presentation of servers will be split into separate categories,
-- so users can differentiate currently used (connected) servers,
-- previously connected servers that were in use in previous sessions,
-- and servers that are only proxied (not connected directly).
data ServersSummary = ServersSummary
{ -- currently used SMP servers are those with Just in sessions and/or subs in SMPServerSummary;
-- all other servers would fall either into previously used or only proxied servers category
currentlyUsedSMPServers :: [SMPServerSummary],
-- previously used SMP servers are those with Nothing in sessions and subs,
-- and have any of sentDirect, sentProxied, recvMsgs, etc. > 0 in server stats (see toPresentedServersSummary);
-- remaining servers would fall into only proxied servers category
previouslyUsedSMPServers :: [SMPServerSummary],
-- only proxied SMP servers are those that aren't (according to current state - sessions and subs)
-- and weren't (according to stats) connected directly; they would have Nothing in sessions and subs,
-- and have all of sentDirect, sentProxied, recvMsgs, etc. = 0 in server stats
onlyProxiedSMPServers :: [SMPServerSummary],
-- currently used XFTP servers are those with Just in sessions in XFTPServerSummary,
-- and/or have upload/download/deletion in progress;
-- all other servers would fall into previously used servers category
currentlyUsedXFTPServers :: [XFTPServerSummary],
-- previously used XFTP servers are those with Nothing in sessions and don't have any process in progress
previouslyUsedXFTPServers :: [XFTPServerSummary]
}
deriving (Show)
data SMPServerSummary = SMPServerSummary
{ smpServer :: SMPServer,
-- known:
-- for simplicity always Nothing in totalServersSummary - allows us to load configured servers only for current user,
-- and also unnecessary unless we want to add navigation to other users servers settings;
-- always Just in currentUserServers - True if server is in list of user servers, otherwise False;
-- True - allows to navigate to server settings, False - allows to add server to configured as known (SEKnown)
known :: Maybe Bool,
sessions :: Maybe ServerSessions,
subs :: Maybe SMPServerSubs,
-- stats:
-- even if sessions and subs are Nothing, stats can be Just - server could be used earlier in session,
-- or in previous sessions and stats for it were restored; server would fall into a category of
-- previously used or only proxied servers - see ServersSummary above
stats :: Maybe AgentSMPServerStatsData
}
deriving (Show)
data XFTPServerSummary = XFTPServerSummary
{ xftpServer :: XFTPServer,
known :: Maybe Bool, -- same as for SMPServerSummary
sessions :: Maybe ServerSessions,
stats :: Maybe AgentXFTPServerStatsData,
rcvInProgress :: Bool,
sndInProgress :: Bool,
delInProgress :: Bool
}
deriving (Show)
-- Maps AgentServersSummary to PresentedServersSummary:
-- - currentUserServers is for currentUser;
-- - users are passed to exclude hidden users from totalServersSummary;
-- - if currentUser is hidden, it should be accounted in totalServersSummary;
-- - known is set only in user level summaries based on passed userSMPSrvs and userXFTPSrvs
toPresentedServersSummary :: AgentServersSummary -> [User] -> User -> [SMPServer] -> [XFTPServer] -> PresentedServersSummary
toPresentedServersSummary agentSummary users currentUser userSMPSrvs userXFTPSrvs = do
let (userSMPSrvsSumms, allSMPSrvsSumms) = accSMPSrvsSummaries
(userSMPCurr, userSMPPrev, userSMPProx) = smpSummsIntoCategories userSMPSrvsSumms
(allSMPCurr, allSMPPrev, allSMPProx) = smpSummsIntoCategories allSMPSrvsSumms
(userXFTPSrvsSumms, allXFTPSrvsSumms) = accXFTPSrvsSummaries
(userXFTPCurr, userXFTPPrev) = xftpSummsIntoCategories userXFTPSrvsSumms
(allXFTPCurr, allXFTPPrev) = xftpSummsIntoCategories allXFTPSrvsSumms
PresentedServersSummary
{ statsStartedAt,
currentUserServers =
ServersSummary
{ currentlyUsedSMPServers = userSMPCurr,
previouslyUsedSMPServers = userSMPPrev,
onlyProxiedSMPServers = userSMPProx,
currentlyUsedXFTPServers = userXFTPCurr,
previouslyUsedXFTPServers = userXFTPPrev
},
allUsersServers =
ServersSummary
{ currentlyUsedSMPServers = allSMPCurr,
previouslyUsedSMPServers = allSMPPrev,
onlyProxiedSMPServers = allSMPProx,
currentlyUsedXFTPServers = allXFTPCurr,
previouslyUsedXFTPServers = allXFTPPrev
}
}
where
AgentServersSummary {statsStartedAt, smpServersSessions, smpServersSubs, smpServersStats, xftpServersSessions, xftpServersStats, xftpRcvInProgress, xftpSndInProgress, xftpDelInProgress} = agentSummary
countUserInAll auId = auId == aUserId currentUser || auId `notElem` hiddenUserIds
hiddenUserIds = map aUserId $ filter (isJust . viewPwdHash) users
smpSummsIntoCategories :: Map SMPServer SMPServerSummary -> ([SMPServerSummary], [SMPServerSummary], [SMPServerSummary])
smpSummsIntoCategories = foldr partitionSummary ([], [], [])
where
partitionSummary srvSumm (curr, prev, prox)
| isCurrentlyUsed srvSumm = (srvSumm : curr, prev, prox)
| isPreviouslyUsed srvSumm = (curr, srvSumm : prev, prox)
| otherwise = (curr, prev, srvSumm : prox)
isCurrentlyUsed SMPServerSummary {sessions, subs} = isJust sessions || isJust subs
isPreviouslyUsed SMPServerSummary {stats} = case stats of
Nothing -> False
-- add connCompleted, connDeleted?
-- check: should connCompleted be counted for proxy? is it?
Just AgentSMPServerStatsData {_sentDirect, _sentProxied, _sentDirectAttempts, _sentProxiedAttempts, _recvMsgs, _connCreated, _connSecured, _connSubscribed, _connSubAttempts} ->
_sentDirect > 0 || _sentProxied > 0 || _sentDirectAttempts > 0 || _sentProxiedAttempts > 0 || _recvMsgs > 0 || _connCreated > 0 || _connSecured > 0 || _connSubscribed > 0 || _connSubAttempts > 0
xftpSummsIntoCategories :: Map XFTPServer XFTPServerSummary -> ([XFTPServerSummary], [XFTPServerSummary])
xftpSummsIntoCategories = foldr partitionSummary ([], [])
where
partitionSummary srvSumm (curr, prev)
| isCurrentlyUsed srvSumm = (srvSumm : curr, prev)
| otherwise = (curr, srvSumm : prev)
isCurrentlyUsed XFTPServerSummary {sessions, rcvInProgress, sndInProgress, delInProgress} =
isJust sessions || rcvInProgress || sndInProgress || delInProgress
accSMPSrvsSummaries :: (Map SMPServer SMPServerSummary, Map SMPServer SMPServerSummary)
accSMPSrvsSummaries = M.foldrWithKey' (addServerData addStats) summs2 smpServersStats
where
summs1 = M.foldrWithKey' (addServerData addSessions) (M.empty, M.empty) smpServersSessions
summs2 = M.foldrWithKey' (addServerData addSubs) summs1 smpServersSubs
addServerData ::
(a -> SMPServerSummary -> SMPServerSummary) ->
(UserId, SMPServer) ->
a ->
(Map SMPServer SMPServerSummary, Map SMPServer SMPServerSummary) ->
(Map SMPServer SMPServerSummary, Map SMPServer SMPServerSummary)
addServerData addData (userId, srv) d (userSumms, allUsersSumms) = (userSumms', allUsersSumms')
where
userSumms'
| userId == aUserId currentUser = alterSumms newUserSummary userSumms
| otherwise = userSumms
allUsersSumms'
| countUserInAll userId = alterSumms newSummary allUsersSumms
| otherwise = allUsersSumms
alterSumms n = M.alter (Just . addData d . fromMaybe n) srv
newUserSummary = (newSummary :: SMPServerSummary) {known = Just $ srv `elem` userSMPSrvs}
newSummary =
SMPServerSummary
{ smpServer = srv,
known = Nothing,
sessions = Nothing,
subs = Nothing,
stats = Nothing
}
addSessions :: ServerSessions -> SMPServerSummary -> SMPServerSummary
addSessions s summ@SMPServerSummary {sessions} = summ {sessions = Just $ maybe s (s `addServerSessions`) sessions}
addSubs :: SMPServerSubs -> SMPServerSummary -> SMPServerSummary
addSubs s summ@SMPServerSummary {subs} = summ {subs = Just $ maybe s (s `addSMPSubs`) subs}
addStats :: AgentSMPServerStatsData -> SMPServerSummary -> SMPServerSummary
addStats s summ@SMPServerSummary {stats} = summ {stats = Just $ maybe s (s `addSMPStats`) stats}
accXFTPSrvsSummaries :: (Map XFTPServer XFTPServerSummary, Map XFTPServer XFTPServerSummary)
accXFTPSrvsSummaries = M.foldrWithKey' (addServerData addStats) summs1 xftpServersStats
where
summs1 = M.foldrWithKey' (addServerData addSessions) (M.empty, M.empty) xftpServersSessions
addServerData ::
(a -> XFTPServerSummary -> XFTPServerSummary) ->
(UserId, XFTPServer) ->
a ->
(Map XFTPServer XFTPServerSummary, Map XFTPServer XFTPServerSummary) ->
(Map XFTPServer XFTPServerSummary, Map XFTPServer XFTPServerSummary)
addServerData addData (userId, srv) d (userSumms, allUsersSumms) = (userSumms', allUsersSumms')
where
userSumms'
| userId == aUserId currentUser = alterSumms newUserSummary userSumms
| otherwise = userSumms
allUsersSumms'
| countUserInAll userId = alterSumms newSummary allUsersSumms
| otherwise = allUsersSumms
alterSumms n = M.alter (Just . addData d . fromMaybe n) srv
newUserSummary = (newSummary :: XFTPServerSummary) {known = Just $ srv `elem` userXFTPSrvs}
newSummary =
XFTPServerSummary
{ xftpServer = srv,
known = Nothing,
sessions = Nothing,
stats = Nothing,
rcvInProgress = srv `elem` xftpRcvInProgress,
sndInProgress = srv `elem` xftpSndInProgress,
delInProgress = srv `elem` xftpDelInProgress
}
addSessions :: ServerSessions -> XFTPServerSummary -> XFTPServerSummary
addSessions s summ@XFTPServerSummary {sessions} = summ {sessions = Just $ maybe s (s `addServerSessions`) sessions}
addStats :: AgentXFTPServerStatsData -> XFTPServerSummary -> XFTPServerSummary
addStats s summ@XFTPServerSummary {stats} = summ {stats = Just $ maybe s (s `addXFTPStats`) stats}
addServerSessions :: ServerSessions -> ServerSessions -> ServerSessions
addServerSessions ss1 ss2 =
ServerSessions
{ ssConnected = ssConnected ss1 + ssConnected ss2,
ssErrors = ssErrors ss1 + ssErrors ss2,
ssConnecting = ssConnecting ss1 + ssConnecting ss2
}
addSMPSubs :: SMPServerSubs -> SMPServerSubs -> SMPServerSubs
addSMPSubs ss1 ss2 =
SMPServerSubs
{ ssActive = ssActive ss1 + ssActive ss2,
ssPending = ssPending ss1 + ssPending ss2
}
addSMPStats :: AgentSMPServerStatsData -> AgentSMPServerStatsData -> AgentSMPServerStatsData
addSMPStats sd1 sd2 =
AgentSMPServerStatsData
{ _sentDirect = _sentDirect sd1 + _sentDirect sd2,
_sentViaProxy = _sentViaProxy sd1 + _sentViaProxy sd2,
_sentProxied = _sentProxied sd1 + _sentProxied sd2,
_sentDirectAttempts = _sentDirectAttempts sd1 + _sentDirectAttempts sd2,
_sentViaProxyAttempts = _sentViaProxyAttempts sd1 + _sentViaProxyAttempts sd2,
_sentProxiedAttempts = _sentProxiedAttempts sd1 + _sentProxiedAttempts sd2,
_sentAuthErrs = _sentAuthErrs sd1 + _sentAuthErrs sd2,
_sentQuotaErrs = _sentQuotaErrs sd1 + _sentQuotaErrs sd2,
_sentExpiredErrs = _sentExpiredErrs sd1 + _sentExpiredErrs sd2,
_sentOtherErrs = _sentOtherErrs sd1 + _sentOtherErrs sd2,
_recvMsgs = _recvMsgs sd1 + _recvMsgs sd2,
_recvDuplicates = _recvDuplicates sd1 + _recvDuplicates sd2,
_recvCryptoErrs = _recvCryptoErrs sd1 + _recvCryptoErrs sd2,
_recvErrs = _recvErrs sd1 + _recvErrs sd2,
_connCreated = _connCreated sd1 + _connCreated sd2,
_connSecured = _connSecured sd1 + _connSecured sd2,
_connCompleted = _connCompleted sd1 + _connCompleted sd2,
_connDeleted = _connDeleted sd1 + _connDeleted sd2,
_connSubscribed = _connSubscribed sd1 + _connSubscribed sd2,
_connSubAttempts = _connSubAttempts sd1 + _connSubAttempts sd2,
_connSubErrs = _connSubErrs sd1 + _connSubErrs sd2
}
addXFTPStats :: AgentXFTPServerStatsData -> AgentXFTPServerStatsData -> AgentXFTPServerStatsData
addXFTPStats sd1 sd2 =
AgentXFTPServerStatsData
{ _uploads = _uploads sd1 + _uploads sd2,
_uploadAttempts = _uploadAttempts sd1 + _uploadAttempts sd2,
_uploadErrs = _uploadErrs sd1 + _uploadErrs sd2,
_downloads = _downloads sd1 + _downloads sd2,
_downloadAttempts = _downloadAttempts sd1 + _downloadAttempts sd2,
_downloadAuthErrs = _downloadAuthErrs sd1 + _downloadAuthErrs sd2,
_downloadErrs = _downloadErrs sd1 + _downloadErrs sd2,
_deletions = _deletions sd1 + _deletions sd2,
_deleteAttempts = _deleteAttempts sd1 + _deleteAttempts sd2,
_deleteErrs = _deleteErrs sd1 + _deleteErrs sd2
}
$(J.deriveJSON defaultJSON ''SMPServerSummary)
$(J.deriveJSON defaultJSON ''XFTPServerSummary)
$(J.deriveJSON defaultJSON ''ServersSummary)
$(J.deriveJSON defaultJSON ''PresentedServersSummary)
+4 -3
View File
@@ -523,9 +523,10 @@ getProtocolServers db User {userId} =
(userId, decodeLatin1 $ strEncode protocol)
where
protocol = protocolTypeI @p
toServerCfg :: (NonEmpty TransportHost, String, C.KeyHash, Maybe Text, Bool, Maybe Bool, Bool) -> ServerCfg p
toServerCfg (host, port, keyHash, auth_, preset, tested, enabled) =
toServerCfg :: (NonEmpty TransportHost, String, C.KeyHash, Maybe Text, Bool, Maybe Bool, Int) -> ServerCfg p
toServerCfg (host, port, keyHash, auth_, preset, tested, enabledInt) =
let server = ProtoServerWithAuth (ProtocolServer protocol host port keyHash) (BasicAuth . encodeUtf8 <$> auth_)
enabled = toServerEnabled enabledInt
in ServerCfg {server, preset, tested, enabled}
overwriteProtocolServers :: forall p. ProtocolTypeI p => DB.Connection -> User -> [ServerCfg p] -> ExceptT StoreError IO ()
@@ -542,7 +543,7 @@ overwriteProtocolServers db User {userId} servers =
(protocol, host, port, key_hash, basic_auth, preset, tested, enabled, user_id, created_at, updated_at)
VALUES (?,?,?,?,?,?,?,?,?,?,?)
|]
((protocol, host, port, keyHash, safeDecodeUtf8 . unBasicAuth <$> auth_) :. (preset, tested, enabled, userId, currentTs, currentTs))
((protocol, host, port, keyHash, safeDecodeUtf8 . unBasicAuth <$> auth_) :. (preset, tested, fromServerEnabled enabled, userId, currentTs, currentTs))
pure $ Right ()
where
protocol = decodeLatin1 $ strEncode $ protocolTypeI @p
+35 -1
View File
@@ -1632,10 +1632,42 @@ data ServerCfg p = ServerCfg
{ server :: ProtoServerWithAuth p,
preset :: Bool,
tested :: Maybe Bool,
enabled :: Bool
enabled :: ServerEnabled
}
deriving (Show)
data ServerEnabled
= SEDisabled
| SEEnabled
| -- server is marked as known, but it's not in the list of configured servers;
-- e.g., it may be added via an unknown server dialogue and user didn't manually configure it,
-- meaning server wasn't tested (or at least such option wasn't presented in UI)
-- and it may be inoperable for user due to server password
SEKnown
deriving (Eq, Show)
pattern DBSEDisabled :: Int
pattern DBSEDisabled = 0
pattern DBSEEnabled :: Int
pattern DBSEEnabled = 1
pattern DBSEKnown :: Int
pattern DBSEKnown = 2
toServerEnabled :: Int -> ServerEnabled
toServerEnabled = \case
DBSEDisabled -> SEDisabled
DBSEEnabled -> SEEnabled
DBSEKnown -> SEKnown
_ -> SEDisabled
fromServerEnabled :: ServerEnabled -> Int
fromServerEnabled = \case
SEDisabled -> DBSEDisabled
SEEnabled -> DBSEEnabled
SEKnown -> DBSEKnown
data ChatVersion
instance VersionScope ChatVersion
@@ -1764,6 +1796,8 @@ $(JQ.deriveJSON defaultJSON ''ContactRef)
$(JQ.deriveJSON defaultJSON ''NoteFolder)
$(JQ.deriveJSON (enumJSON $ dropPrefix "SE") ''ServerEnabled)
instance ProtocolTypeI p => ToJSON (ServerCfg p) where
toEncoding = $(JQ.mkToEncoding defaultJSON ''ServerCfg)
toJSON = $(JQ.mkToJSON defaultJSON ''ServerCfg)
+1 -1
View File
@@ -36,7 +36,7 @@ import Simplex.Chat.Types.Shared
import Simplex.Chat.Types.Util
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON, fromTextField_, sumTypeJSON)
import Simplex.Messaging.Util (safeDecodeUtf8, (<$?>))
import Simplex.Messaging.Util (decodeJSON, encodeJSON, safeDecodeUtf8, (<$?>))
data ChatFeature
= CFTimedMessages
+1
View File
@@ -18,6 +18,7 @@ import Database.SQLite.Simple.ToField (ToField (..))
import Simplex.Chat.Types.Util
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON, fromTextField_)
import Simplex.Messaging.Util (decodeJSON, encodeJSON)
data UITheme = UITheme
{ themeId :: Text,
-11
View File
@@ -2,26 +2,15 @@
module Simplex.Chat.Types.Util where
import Data.Aeson (FromJSON, ToJSON)
import qualified Data.Aeson as J
import qualified Data.Aeson.Types as JT
import Data.ByteString (ByteString)
import qualified Data.ByteString.Lazy.Char8 as LB
import Data.Text (Text)
import Data.Text.Encoding (encodeUtf8)
import Data.Typeable
import Database.SQLite.Simple (ResultError (..), SQLData (..))
import Database.SQLite.Simple.FromField (FieldParser, returnError)
import Database.SQLite.Simple.Internal (Field (..))
import Database.SQLite.Simple.Ok (Ok (Ok))
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Util (safeDecodeUtf8)
encodeJSON :: ToJSON a => a -> Text
encodeJSON = safeDecodeUtf8 . LB.toStrict . J.encode
decodeJSON :: FromJSON a => Text -> Maybe a
decodeJSON = J.decode . LB.fromStrict . encodeUtf8
textParseJSON :: TextEncoding a => String -> J.Value -> JT.Parser a
textParseJSON name = J.withText name $ maybe (fail $ "bad " <> name) pure . textDecode
+1 -3
View File
@@ -54,7 +54,6 @@ import qualified Simplex.FileTransfer.Transport as XFTP
import Simplex.Messaging.Agent.Client (ProtocolTestFailure (..), ProtocolTestStep (..), SubscriptionsInfo (..))
import Simplex.Messaging.Agent.Env.SQLite (NetworkConfig (..))
import Simplex.Messaging.Agent.Protocol
import Simplex.Messaging.Agent.Protocol (AgentErrorType (RCP))
import Simplex.Messaging.Agent.Store.SQLite.DB (SlowQueryStats (..))
import Simplex.Messaging.Client (SMPProxyFallback, SMPProxyMode (..))
import qualified Simplex.Messaging.Crypto as C
@@ -366,7 +365,7 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe
"chat entity locks: " <> viewJSON chatEntityLocks,
"agent locks: " <> viewJSON agentLocks
]
CRAgentStats stats -> map (plain . intercalate ",") stats
CRAgentServersSummary u serversSummary -> ttyUser u ["agent servers summary: " <> viewJSON serversSummary]
CRAgentSubs {activeSubs, pendingSubs, removedSubs} ->
[plain $ "Subscriptions: active = " <> show (sum activeSubs) <> ", pending = " <> show (sum pendingSubs) <> ", removed = " <> show (sum $ M.map length removedSubs)]
<> ("active subscriptions:" : listSubs activeSubs)
@@ -384,7 +383,6 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe
[ "agent workers details:",
viewJSON agentWorkersDetails -- this would be huge, but copypastable when has its own line
]
CRAgentMsgCounts {msgCounts} -> ["received messages (total, duplicates):", viewJSON msgCounts]
CRAgentQueuesInfo {agentQueuesInfo} ->
[ "agent queues info:",
plain . LB.unpack $ J.encode agentQueuesInfo
+1 -1
View File
@@ -17,7 +17,7 @@ import Simplex.Chat.Store.Shared (createContact)
import Simplex.Chat.Types (ConnStatus (..), Profile (..))
import Simplex.Chat.Types.Shared (GroupMemberRole (..))
import Simplex.Chat.Types.UITheme
import Simplex.Chat.Types.Util (encodeJSON)
import Simplex.Messaging.Util (encodeJSON)
import Simplex.Messaging.Encoding.String (StrEncoding (..))
import System.Directory (copyFile, createDirectoryIfMissing)
import Test.Hspec hiding (it)
+5
View File
@@ -149,6 +149,11 @@ textWithUri = describe "text with Uri" do
parseMarkdown "http://simplex.chat" `shouldBe` uri "http://simplex.chat"
parseMarkdown "this is https://simplex.chat" `shouldBe` "this is " <> uri "https://simplex.chat"
parseMarkdown "https://simplex.chat site" `shouldBe` uri "https://simplex.chat" <> " site"
parseMarkdown "SimpleX on GitHub: https://github.com/simplex-chat/" `shouldBe` "SimpleX on GitHub: " <> uri "https://github.com/simplex-chat/"
parseMarkdown "SimpleX on GitHub: https://github.com/simplex-chat." `shouldBe` "SimpleX on GitHub: " <> uri "https://github.com/simplex-chat" <> "."
parseMarkdown "https://github.com/simplex-chat/ - SimpleX on GitHub" `shouldBe` uri "https://github.com/simplex-chat/" <> " - SimpleX on GitHub"
-- parseMarkdown "SimpleX on GitHub (https://github.com/simplex-chat/)" `shouldBe` "SimpleX on GitHub (" <> uri "https://github.com/simplex-chat/" <> ")"
parseMarkdown "https://en.m.wikipedia.org/wiki/Servo_(software)" `shouldBe` uri "https://en.m.wikipedia.org/wiki/Servo_(software)"
it "ignored as markdown" do
parseMarkdown "_https://simplex.chat" `shouldBe` "_https://simplex.chat"
parseMarkdown "this is _https://simplex.chat" `shouldBe` "this is _https://simplex.chat"
+12 -11
View File
@@ -34,7 +34,8 @@ queue =
SMPQueueAddress
{ smpServer = srv,
senderId = "\223\142z\251",
dhPublicKey = "MCowBQYDK2VuAyEAjiswwI3O/NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o="
dhPublicKey = "MCowBQYDK2VuAyEAjiswwI3O/NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o=",
sndSecure = False
}
connReqData :: ConnReqUriData
@@ -194,7 +195,7 @@ decodeChatMessageTest = describe "Chat message encoding/decoding" $ do
"{\"v\":\"1\",\"event\":\"x.msg.deleted\",\"params\":{}}"
#==# XMsgDeleted
it "x.file" $
"{\"v\":\"1\",\"event\":\"x.file\",\"params\":{\"file\":{\"fileConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"fileSize\":12345,\"fileName\":\"photo.jpg\"}}}"
"{\"v\":\"1\",\"event\":\"x.file\",\"params\":{\"file\":{\"fileConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-3%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"fileSize\":12345,\"fileName\":\"photo.jpg\"}}}"
#==# XFile FileInvitation {fileName = "photo.jpg", fileSize = 12345, fileDigest = Nothing, fileConnReq = Just testConnReq, fileInline = Nothing, fileDescr = Nothing}
it "x.file without file invitation" $
"{\"v\":\"1\",\"event\":\"x.file\",\"params\":{\"file\":{\"fileSize\":12345,\"fileName\":\"photo.jpg\"}}}"
@@ -203,7 +204,7 @@ decodeChatMessageTest = describe "Chat message encoding/decoding" $ do
"{\"v\":\"1\",\"event\":\"x.file.acpt\",\"params\":{\"fileName\":\"photo.jpg\"}}"
#==# XFileAcpt "photo.jpg"
it "x.file.acpt.inv" $
"{\"v\":\"1\",\"event\":\"x.file.acpt.inv\",\"params\":{\"msgId\":\"AQIDBA==\",\"fileName\":\"photo.jpg\",\"fileConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"}}"
"{\"v\":\"1\",\"event\":\"x.file.acpt.inv\",\"params\":{\"msgId\":\"AQIDBA==\",\"fileName\":\"photo.jpg\",\"fileConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-3%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"}}"
#==# XFileAcptInv (SharedMsgId "\1\2\3\4") (Just testConnReq) "photo.jpg"
it "x.file.acpt.inv" $
"{\"v\":\"1\",\"event\":\"x.file.acpt.inv\",\"params\":{\"msgId\":\"AQIDBA==\",\"fileName\":\"photo.jpg\"}}"
@@ -230,10 +231,10 @@ decodeChatMessageTest = describe "Chat message encoding/decoding" $ do
"{\"v\":\"1\",\"event\":\"x.contact\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"},\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}"
==# XContact testProfile Nothing
it "x.grp.inv" $
"{\"v\":\"1\",\"event\":\"x.grp.inv\",\"params\":{\"groupInvitation\":{\"connRequest\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"invitedMember\":{\"memberRole\":\"member\",\"memberId\":\"BQYHCA==\"},\"groupProfile\":{\"fullName\":\"Team\",\"displayName\":\"team\",\"groupPreferences\":{\"reactions\":{\"enable\":\"on\"},\"voice\":{\"enable\":\"on\"}}},\"fromMember\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\"}}}}"
"{\"v\":\"1\",\"event\":\"x.grp.inv\",\"params\":{\"groupInvitation\":{\"connRequest\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-3%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"invitedMember\":{\"memberRole\":\"member\",\"memberId\":\"BQYHCA==\"},\"groupProfile\":{\"fullName\":\"Team\",\"displayName\":\"team\",\"groupPreferences\":{\"reactions\":{\"enable\":\"on\"},\"voice\":{\"enable\":\"on\"}}},\"fromMember\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\"}}}}"
#==# XGrpInv GroupInvitation {fromMember = MemberIdRole (MemberId "\1\2\3\4") GRAdmin, invitedMember = MemberIdRole (MemberId "\5\6\7\8") GRMember, connRequest = testConnReq, groupProfile = testGroupProfile, groupLinkId = Nothing, groupSize = Nothing}
it "x.grp.inv with group link id" $
"{\"v\":\"1\",\"event\":\"x.grp.inv\",\"params\":{\"groupInvitation\":{\"connRequest\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"invitedMember\":{\"memberRole\":\"member\",\"memberId\":\"BQYHCA==\"},\"groupProfile\":{\"fullName\":\"Team\",\"displayName\":\"team\",\"groupPreferences\":{\"reactions\":{\"enable\":\"on\"},\"voice\":{\"enable\":\"on\"}}},\"fromMember\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\"}, \"groupLinkId\":\"AQIDBA==\"}}}"
"{\"v\":\"1\",\"event\":\"x.grp.inv\",\"params\":{\"groupInvitation\":{\"connRequest\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-3%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"invitedMember\":{\"memberRole\":\"member\",\"memberId\":\"BQYHCA==\"},\"groupProfile\":{\"fullName\":\"Team\",\"displayName\":\"team\",\"groupPreferences\":{\"reactions\":{\"enable\":\"on\"},\"voice\":{\"enable\":\"on\"}}},\"fromMember\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\"}, \"groupLinkId\":\"AQIDBA==\"}}}"
#==# XGrpInv GroupInvitation {fromMember = MemberIdRole (MemberId "\1\2\3\4") GRAdmin, invitedMember = MemberIdRole (MemberId "\5\6\7\8") GRMember, connRequest = testConnReq, groupProfile = testGroupProfile, groupLinkId = Just $ GroupLinkId "\1\2\3\4", groupSize = Nothing}
it "x.grp.acpt without incognito profile" $
"{\"v\":\"1\",\"event\":\"x.grp.acpt\",\"params\":{\"memberId\":\"AQIDBA==\"}}"
@@ -254,16 +255,16 @@ decodeChatMessageTest = describe "Chat message encoding/decoding" $ do
"{\"v\":\"1\",\"event\":\"x.grp.mem.intro\",\"params\":{\"memberRestrictions\":{\"restriction\":\"blocked\"},\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}"
#==# XGrpMemIntro MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, v = Nothing, profile = testProfile} (Just MemberRestrictions {restriction = MRSBlocked})
it "x.grp.mem.inv" $
"{\"v\":\"1\",\"event\":\"x.grp.mem.inv\",\"params\":{\"memberId\":\"AQIDBA==\",\"memberIntro\":{\"directConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"groupConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"}}}"
"{\"v\":\"1\",\"event\":\"x.grp.mem.inv\",\"params\":{\"memberId\":\"AQIDBA==\",\"memberIntro\":{\"directConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-3%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"groupConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-3%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"}}}"
#==# XGrpMemInv (MemberId "\1\2\3\4") IntroInvitation {groupConnReq = testConnReq, directConnReq = Just testConnReq}
it "x.grp.mem.inv w/t directConnReq" $
"{\"v\":\"1\",\"event\":\"x.grp.mem.inv\",\"params\":{\"memberId\":\"AQIDBA==\",\"memberIntro\":{\"groupConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"}}}"
"{\"v\":\"1\",\"event\":\"x.grp.mem.inv\",\"params\":{\"memberId\":\"AQIDBA==\",\"memberIntro\":{\"groupConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-3%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"}}}"
#==# XGrpMemInv (MemberId "\1\2\3\4") IntroInvitation {groupConnReq = testConnReq, directConnReq = Nothing}
it "x.grp.mem.fwd" $
"{\"v\":\"1\",\"event\":\"x.grp.mem.fwd\",\"params\":{\"memberIntro\":{\"directConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"groupConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"},\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}"
"{\"v\":\"1\",\"event\":\"x.grp.mem.fwd\",\"params\":{\"memberIntro\":{\"directConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-3%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"groupConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-3%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"},\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}"
#==# XGrpMemFwd MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, v = Nothing, profile = testProfile} IntroInvitation {groupConnReq = testConnReq, directConnReq = Just testConnReq}
it "x.grp.mem.fwd with member chat version range and w/t directConnReq" $
"{\"v\":\"1\",\"event\":\"x.grp.mem.fwd\",\"params\":{\"memberIntro\":{\"groupConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"},\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"v\":\"1-8\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}"
"{\"v\":\"1\",\"event\":\"x.grp.mem.fwd\",\"params\":{\"memberIntro\":{\"groupConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-3%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"},\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"v\":\"1-8\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}"
#==# XGrpMemFwd MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, v = Just $ ChatVersionRange supportedChatVRange, profile = testProfile} IntroInvitation {groupConnReq = testConnReq, directConnReq = Nothing}
it "x.grp.mem.info" $
"{\"v\":\"1\",\"event\":\"x.grp.mem.info\",\"params\":{\"memberId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}"
@@ -284,10 +285,10 @@ decodeChatMessageTest = describe "Chat message encoding/decoding" $ do
"{\"v\":\"1\",\"event\":\"x.grp.del\",\"params\":{}}"
==# XGrpDel
it "x.grp.direct.inv" $
"{\"v\":\"1\",\"event\":\"x.grp.direct.inv\",\"params\":{\"connReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\", \"content\":{\"text\":\"hello\",\"type\":\"text\"}}}"
"{\"v\":\"1\",\"event\":\"x.grp.direct.inv\",\"params\":{\"connReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-3%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\", \"content\":{\"text\":\"hello\",\"type\":\"text\"}}}"
#==# XGrpDirectInv testConnReq (Just $ MCText "hello")
it "x.grp.direct.inv without content" $
"{\"v\":\"1\",\"event\":\"x.grp.direct.inv\",\"params\":{\"connReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"}}"
"{\"v\":\"1\",\"event\":\"x.grp.direct.inv\",\"params\":{\"connReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-3%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"}}"
#==# XGrpDirectInv testConnReq Nothing
-- it "x.grp.msg.forward"
-- $ "{\"v\":\"1\",\"event\":\"x.grp.msg.forward\",\"params\":{\"msgForward\":{\"memberId\":\"AQIDBA==\",\"msg\":\"{\"v\":\"1\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"}}}\",\"msgTs\":\"1970-01-01T00:00:01.000000001Z\"}}}"
@@ -0,0 +1 @@
ae8b5b2e-76c9-4a31-a044-bcbda1cdf264