Merge branch 'master' into f/batch-send

This commit is contained in:
spaced4ndy
2023-12-20 11:17:33 +04:00
24 changed files with 338 additions and 201 deletions
+96 -61
View File
@@ -14,11 +14,14 @@ struct ContentView: View {
@ObservedObject var alertManager = AlertManager.shared
@ObservedObject var callController = CallController.shared
@Environment(\.colorScheme) var colorScheme
@Binding var doAuthenticate: Bool
@Binding var userAuthorized: Bool?
@Binding var canConnectCall: Bool
@Binding var lastSuccessfulUnlock: TimeInterval?
@Binding var showInitializationView: Bool
var contentAccessAuthenticationExtended: Bool
@Environment(\.scenePhase) var scenePhase
@State private var automaticAuthenticationAttempted = false
@State private var canConnectViewCall = false
@State private var lastSuccessfulUnlock: TimeInterval? = nil
@AppStorage(DEFAULT_SHOW_LA_NOTICE) private var prefShowLANotice = false
@AppStorage(DEFAULT_LA_NOTICE_SHOWN) private var prefLANoticeShown = false
@AppStorage(DEFAULT_PERFORM_LA) private var prefPerformLA = false
@@ -40,9 +43,19 @@ struct ContentView: View {
}
}
private var accessAuthenticated: Bool {
chatModel.contentViewAccessAuthenticated || contentAccessAuthenticationExtended
}
var body: some View {
ZStack {
contentView()
// contentView() has to be in a single branch, so that enabling authentication doesn't trigger re-rendering and close settings.
// i.e. with separate branches like this settings are closed: `if prefPerformLA { ... contentView() ... } else { contentView() }
if !prefPerformLA || accessAuthenticated {
contentView()
} else {
lockButton()
}
if chatModel.showCallView, let call = chatModel.activeCall {
callView(call)
}
@@ -50,6 +63,7 @@ struct ContentView: View {
LocalAuthView(authRequest: la)
} else if showSetPasscode {
SetAppPasscodeView {
chatModel.contentViewAccessAuthenticated = true
prefPerformLA = true
showSetPasscode = false
privacyLocalAuthModeDefault.set(.passcode)
@@ -60,13 +74,9 @@ struct ContentView: View {
alertManager.showAlert(laPasscodeNotSetAlert())
}
}
}
.onAppear {
if prefPerformLA { requestNtfAuthorization() }
initAuthenticate()
}
.onChange(of: doAuthenticate) { _ in
initAuthenticate()
if chatModel.chatDbStatus == nil {
initializationView()
}
}
.alert(isPresented: $alertManager.presentAlert) { alertManager.alertView! }
.sheet(isPresented: $showSettings) {
@@ -76,14 +86,44 @@ struct ContentView: View {
Button("System authentication") { initialEnableLA() }
Button("Passcode entry") { showSetPasscode = true }
}
.onChange(of: scenePhase) { phase in
logger.debug("scenePhase was \(String(describing: scenePhase)), now \(String(describing: phase))")
switch (phase) {
case .background:
// also see .onChange(of: scenePhase) in SimpleXApp: on entering background
// it remembers enteredBackgroundAuthenticated and sets chatModel.contentViewAccessAuthenticated to false
automaticAuthenticationAttempted = false
canConnectViewCall = false
case .active:
canConnectViewCall = !prefPerformLA || contentAccessAuthenticationExtended || unlockedRecently()
// condition `!chatModel.contentViewAccessAuthenticated` is required for when authentication is enabled in settings or on initial notice
if prefPerformLA && !chatModel.contentViewAccessAuthenticated {
if AppChatState.shared.value != .stopped {
if contentAccessAuthenticationExtended {
chatModel.contentViewAccessAuthenticated = true
} else {
if !automaticAuthenticationAttempted {
automaticAuthenticationAttempted = true
// authenticate if call kit call is not in progress
if !(CallController.useCallKit() && chatModel.showCallView && chatModel.activeCall != nil) {
authenticateContentViewAccess()
}
}
}
} else {
// when app is stopped automatic authentication is not attempted
chatModel.contentViewAccessAuthenticated = contentAccessAuthenticationExtended
}
}
default:
break
}
}
}
@ViewBuilder private func contentView() -> some View {
if prefPerformLA && userAuthorized != true {
lockButton()
} else if chatModel.chatDbStatus == nil && showInitializationView {
initializationView()
} else if let status = chatModel.chatDbStatus, status != .ok {
if let status = chatModel.chatDbStatus, status != .ok {
DatabaseErrorView(status: status)
} else if !chatModel.v3DBMigration.startChat {
MigrateToAppGroupView()
@@ -106,11 +146,11 @@ struct ContentView: View {
if CallController.useCallKit() {
ActiveCallView(call: call, canConnectCall: Binding.constant(true))
.onDisappear {
if userAuthorized == false && doAuthenticate { runAuthenticate() }
if prefPerformLA && !accessAuthenticated { authenticateContentViewAccess() }
}
} else {
ActiveCallView(call: call, canConnectCall: $canConnectCall)
if prefPerformLA && userAuthorized != true {
ActiveCallView(call: call, canConnectCall: $canConnectViewCall)
if prefPerformLA && !accessAuthenticated {
Rectangle()
.fill(colorScheme == .dark ? .black : .white)
.frame(maxWidth: .infinity, maxHeight: .infinity)
@@ -120,22 +160,27 @@ struct ContentView: View {
}
private func lockButton() -> some View {
Button(action: runAuthenticate) { Label("Unlock", systemImage: "lock") }
Button(action: authenticateContentViewAccess) { Label("Unlock", systemImage: "lock") }
}
private func initializationView() -> some View {
VStack {
ProgressView().scaleEffect(2)
Text("Opening database…")
Text("Opening app…")
.padding()
}
.frame(maxWidth: .infinity, maxHeight: .infinity )
.background(
Rectangle()
.fill(.background)
)
}
private func mainView() -> some View {
ZStack(alignment: .top) {
ChatListView(showSettings: $showSettings).privacySensitive(protectScreen)
.onAppear {
if !prefPerformLA { requestNtfAuthorization() }
requestNtfAuthorization()
// Local Authentication notice is to be shown on next start after onboarding is complete
if (!prefLANoticeShown && prefShowLANotice && !chatModel.chats.isEmpty) {
prefLANoticeShown = true
@@ -187,48 +232,37 @@ struct ContentView: View {
}
}
private func initAuthenticate() {
logger.debug("initAuthenticate")
if CallController.useCallKit() && chatModel.showCallView && chatModel.activeCall != nil {
userAuthorized = false
} else if doAuthenticate {
runAuthenticate()
}
}
private func runAuthenticate() {
logger.debug("DEBUGGING: runAuthenticate")
if !prefPerformLA {
userAuthorized = true
private func unlockedRecently() -> Bool {
if let lastSuccessfulUnlock = lastSuccessfulUnlock {
return ProcessInfo.processInfo.systemUptime - lastSuccessfulUnlock < 2
} else {
logger.debug("DEBUGGING: before dismissAllSheets")
dismissAllSheets(animated: false) {
logger.debug("DEBUGGING: in dismissAllSheets callback")
chatModel.chatId = nil
justAuthenticate()
}
return false
}
}
private func justAuthenticate() {
userAuthorized = false
let laMode = privacyLocalAuthModeDefault.get()
authenticate(reason: NSLocalizedString("Unlock app", comment: "authentication reason"), selfDestruct: true) { laResult in
logger.debug("DEBUGGING: authenticate callback: \(String(describing: laResult))")
switch (laResult) {
case .success:
userAuthorized = true
canConnectCall = true
lastSuccessfulUnlock = ProcessInfo.processInfo.systemUptime
case .failed:
if laMode == .passcode {
AlertManager.shared.showAlert(laFailedAlert())
private func authenticateContentViewAccess() {
logger.debug("DEBUGGING: authenticateContentViewAccess")
dismissAllSheets(animated: false) {
logger.debug("DEBUGGING: authenticateContentViewAccess, in dismissAllSheets callback")
chatModel.chatId = nil
authenticate(reason: NSLocalizedString("Unlock app", comment: "authentication reason"), selfDestruct: true) { laResult in
logger.debug("DEBUGGING: authenticate callback: \(String(describing: laResult))")
switch (laResult) {
case .success:
chatModel.contentViewAccessAuthenticated = true
canConnectViewCall = true
lastSuccessfulUnlock = ProcessInfo.processInfo.systemUptime
case .failed:
chatModel.contentViewAccessAuthenticated = false
if privacyLocalAuthModeDefault.get() == .passcode {
AlertManager.shared.showAlert(laFailedAlert())
}
case .unavailable:
prefPerformLA = false
canConnectViewCall = true
AlertManager.shared.showAlert(laUnavailableTurningOffAlert())
}
case .unavailable:
userAuthorized = true
prefPerformLA = false
canConnectCall = true
AlertManager.shared.showAlert(laUnavailableTurningOffAlert())
}
}
}
@@ -259,6 +293,7 @@ struct ContentView: View {
authenticate(reason: NSLocalizedString("Enable SimpleX Lock", comment: "authentication reason")) { laResult in
switch laResult {
case .success:
chatModel.contentViewAccessAuthenticated = true
prefPerformLA = true
alertManager.showAlert(laTurnedOnAlert())
case .failed:
+2
View File
@@ -54,6 +54,8 @@ final class ChatModel: ObservableObject {
@Published var chatDbChanged = false
@Published var chatDbEncrypted: Bool?
@Published var chatDbStatus: DBMigrationResult?
// local authentication
@Published var contentViewAccessAuthenticated: Bool = false
@Published var laRequest: LocalAuthRequest?
// list of chat "previews"
@Published var chats: [Chat] = []
+18 -34
View File
@@ -16,18 +16,13 @@ struct SimpleXApp: App {
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
@StateObject private var chatModel = ChatModel.shared
@ObservedObject var alertManager = AlertManager.shared
@Environment(\.scenePhase) var scenePhase
@AppStorage(DEFAULT_PERFORM_LA) private var prefPerformLA = false
@State private var userAuthorized: Bool?
@State private var doAuthenticate = false
@State private var enteredBackground: TimeInterval? = nil
@State private var canConnectCall = false
@State private var lastSuccessfulUnlock: TimeInterval? = nil
@State private var showInitializationView = false
@State private var enteredBackgroundAuthenticated: TimeInterval? = nil
init() {
// DispatchQueue.global(qos: .background).sync {
haskell_init()
haskell_init()
// hs_init(0, nil)
// }
UserDefaults.standard.register(defaults: appDefaults)
@@ -39,21 +34,16 @@ struct SimpleXApp: App {
}
var body: some Scene {
return WindowGroup {
ContentView(
doAuthenticate: $doAuthenticate,
userAuthorized: $userAuthorized,
canConnectCall: $canConnectCall,
lastSuccessfulUnlock: $lastSuccessfulUnlock,
showInitializationView: $showInitializationView
)
WindowGroup {
// contentAccessAuthenticationExtended has to be passed to ContentView on view initialization,
// so that it's computed by the time view renders, and not on event after rendering
ContentView(contentAccessAuthenticationExtended: !authenticationExpired())
.environmentObject(chatModel)
.onOpenURL { url in
logger.debug("ContentView.onOpenURL: \(url)")
chatModel.appOpenUrl = url
}
.onAppear() {
showInitializationView = true
DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) {
initChatAndMigrate()
}
@@ -62,21 +52,25 @@ struct SimpleXApp: App {
logger.debug("scenePhase was \(String(describing: scenePhase)), now \(String(describing: phase))")
switch (phase) {
case .background:
// --- authentication
// see ContentView .onChange(of: scenePhase) for remaining authentication logic
if chatModel.contentViewAccessAuthenticated {
enteredBackgroundAuthenticated = ProcessInfo.processInfo.systemUptime
}
chatModel.contentViewAccessAuthenticated = false
// authentication ---
if CallController.useCallKit() && chatModel.activeCall != nil {
CallController.shared.shouldSuspendChat = true
} else {
suspendChat()
BGManager.shared.schedule()
}
if userAuthorized == true {
enteredBackground = ProcessInfo.processInfo.systemUptime
}
doAuthenticate = false
canConnectCall = false
NtfManager.shared.setNtfBadgeCount(chatModel.totalUnreadCountForAllUsers())
case .active:
CallController.shared.shouldSuspendChat = false
let appState = AppChatState.shared.value
if appState != .stopped {
startChatAndActivate {
if appState.inactive && chatModel.chatRunning == true {
@@ -85,8 +79,6 @@ struct SimpleXApp: App {
updateCallInvitations()
}
}
doAuthenticate = authenticationExpired()
canConnectCall = !(doAuthenticate && prefPerformLA) || unlockedRecently()
}
}
default:
@@ -121,22 +113,14 @@ struct SimpleXApp: App {
}
private func authenticationExpired() -> Bool {
if let enteredBackground = enteredBackground {
if let enteredBackgroundAuthenticated = enteredBackgroundAuthenticated {
let delay = Double(UserDefaults.standard.integer(forKey: DEFAULT_LA_LOCK_DELAY))
return ProcessInfo.processInfo.systemUptime - enteredBackground >= delay
return ProcessInfo.processInfo.systemUptime - enteredBackgroundAuthenticated >= delay
} else {
return true
}
}
private func unlockedRecently() -> Bool {
if let lastSuccessfulUnlock = lastSuccessfulUnlock {
return ProcessInfo.processInfo.systemUptime - lastSuccessfulUnlock < 2
} else {
return false
}
}
private func updateChats() {
do {
let chats = try apiGetChats()
@@ -467,6 +467,7 @@ struct SimplexLockView: View {
switch a {
case .enableAuth:
SetAppPasscodeView {
m.contentViewAccessAuthenticated = true
laLockDelay = 30
prefPerformLA = true
showChangePassword = true
@@ -619,6 +620,7 @@ struct SimplexLockView: View {
authenticate(reason: NSLocalizedString("Enable SimpleX Lock", comment: "authentication reason")) { laResult in
switch laResult {
case .success:
m.contentViewAccessAuthenticated = true
prefPerformLA = true
laAlert = .laTurnedOnAlert
case .failed:
+60 -4
View File
@@ -155,6 +155,34 @@ afterEvaluate {
val endTagRegex = Regex("</")
val anyHtmlRegex = Regex("[^>]*>.*(<|>).*</string>|[^>]*>.*(&lt;|&gt;).*</string>")
val correctHtmlRegex = Regex("[^>]*>.*<b>.*</b>.*</string>|[^>]*>.*<i>.*</i>.*</string>|[^>]*>.*<u>.*</u>.*</string>|[^>]*>.*<font[^>]*>.*</font>.*</string>")
val possibleFormat = listOf("s", "d", "1\$s", "1\$d", "2s", "f")
fun String.id(): String = replace("<string name=\"", "").trim().substringBefore("\"")
fun String.formatting(filepath: String): List<String> {
if (!contains("%")) return emptyList()
val value = substringAfter("\">").substringBeforeLast("</string>")
val formats = ArrayList<String>()
var substring = value.substringAfter("%")
while (true) {
var foundFormat = false
for (format in possibleFormat) {
if (substring.startsWith(format)) {
formats.add(format)
foundFormat = true
break
}
}
if (!foundFormat) {
throw Exception("Unknown formatting in string. Add it to 'possibleFormat' in common/build.gradle.kts if needed: $this \nin $filepath")
}
val was = substring
substring = substring.substringAfter("%")
if (was.length == substring.length) break
}
return formats
}
fun String.removeCDATA(): String =
if (contains("<![CDATA")) {
@@ -195,20 +223,44 @@ afterEvaluate {
return this
}
val fileRegex = Regex("MR/../strings.xml$|MR/..-.../strings.xml$|MR/..-../strings.xml$|MR/base/strings.xml$")
kotlin.sourceSets["commonMain"].resources.filter { fileRegex.containsMatchIn(it.absolutePath) }.asFileTree.forEach { file ->
val tree = kotlin.sourceSets["commonMain"].resources.filter { fileRegex.containsMatchIn(it.absolutePath) }.asFileTree
val baseStringsFile = tree.first { it.absolutePath.endsWith("base/strings.xml") } ?: throw Exception("No base/strings.xml found")
val treeList = ArrayList(tree.toList())
treeList.remove(baseStringsFile)
treeList.add(0, baseStringsFile)
val baseFormatting = mutableMapOf<String, List<String>>()
treeList.forEachIndexed { index, file ->
val isBase = index == 0
val initialLines = ArrayList<String>()
val finalLines = ArrayList<String>()
val errors = ArrayList<String>()
file.useLines { lines ->
val multiline = ArrayList<String>()
lines.forEach { line ->
initialLines.add(line)
if (stringRegex.matches(line)) {
finalLines.add(line.removeCDATA().addCDATA(file.absolutePath))
val fixedLine = line.removeCDATA().addCDATA(file.absolutePath)
val lineId = fixedLine.id()
if (isBase) {
baseFormatting[lineId] = fixedLine.formatting(file.absolutePath)
} else if (baseFormatting[lineId] != fixedLine.formatting(file.absolutePath)) {
errors.add("Incorrect formatting in string: $fixedLine \nin ${file.absolutePath}")
}
finalLines.add(fixedLine)
} else if (multiline.isEmpty() && startStringRegex.containsMatchIn(line)) {
multiline.add(line)
} else if (multiline.isNotEmpty() && endStringRegex.containsMatchIn(line)) {
multiline.add(line)
finalLines.addAll(multiline.joinToString("\n").removeCDATA().addCDATA(file.absolutePath).split("\n"))
val fixedLines = multiline.joinToString("\n").removeCDATA().addCDATA(file.absolutePath).split("\n")
val fixedLinesJoined = fixedLines.joinToString("")
val lineId = fixedLinesJoined.id()
if (isBase) {
baseFormatting[lineId] = fixedLinesJoined.formatting(file.absolutePath)
} else if (baseFormatting[lineId] != fixedLinesJoined.formatting(file.absolutePath)) {
errors.add("Incorrect formatting in string: $fixedLinesJoined \nin ${file.absolutePath}")
}
finalLines.addAll(fixedLines)
multiline.clear()
} else if (multiline.isNotEmpty()) {
multiline.add(line)
@@ -217,10 +269,14 @@ afterEvaluate {
}
}
if (multiline.isNotEmpty()) {
throw Exception("Unclosed string tag: ${multiline.joinToString("\n")} \nin ${file.absolutePath}")
errors.add("Unclosed string tag: ${multiline.joinToString("\n")} \nin ${file.absolutePath}")
}
}
if (errors.isNotEmpty()) {
throw Exception("Found errors: \n\n${errors.joinToString("\n\n")}")
}
if (!debug && finalLines != initialLines) {
file.writer().use {
finalLines.forEachIndexed { index, line ->
@@ -67,7 +67,7 @@ fun QRCode(
scope.launch {
val image = qrCodeBitmap(connReq, 1024).replaceColor(Color.Black.toArgb(), tintColor.toArgb())
.let { if (withLogo) it.addLogo() else it }
val file = saveTempImageUncompressed(image, false)
val file = saveTempImageUncompressed(image, true)
if (file != null) {
shareFile("", CryptoFile.plain(file.absolutePath))
}
@@ -150,7 +150,6 @@
<string name="add_new_contact_to_create_one_time_QR_code"><![CDATA[<b> إضافة جهة اتصال جديدة </b>: لإنشاء رمز الاستجابة السريعة الخاص بك لمرة واحدة لجهة اتصالك.]]></string>
<string name="scan_QR_code_to_connect_to_contact_who_shows_QR_code"><![CDATA[<b> امسح رمز الاستجابة السريعة </b>: للاتصال بجهة الاتصال التي تعرض لك رمز الاستجابة السريعة.]]></string>
<string name="callstatus_in_progress">مكالمتك تحت الإجراء</string>
<string name="callstatus_ended">انتهت المكالمة</string>
<string name="change_database_passphrase_question">تغيير عبارة مرور قاعدة البيانات؟</string>
<string name="cannot_access_keychain">لا يمكن الوصول إلى Keystore لحفظ كلمة مرور قاعدة البيانات</string>
<string name="icon_descr_cancel_file_preview">إلغاء معاينة الملف</string>
@@ -41,7 +41,7 @@
<string name="accept">Αποδοχή</string>
<string name="accept_connection_request__question">Αποδοχή αιτήματος σύνδεσης;</string>
<string name="callstatus_accepted">αποδεκτή κλήση</string>
<string name="network_enable_socks_info">Πρόσβαση στους διακομιστές μέσω SOCKS proxy στην πόρτα 9050; Ο διακομιστής μεσολάβησης (proxy server) πρέπει να είναι ενεργός πριν ενεργοποιηθεί αυτή η ρύθμιση.</string>
<string name="network_enable_socks_info">Πρόσβαση στους διακομιστές μέσω SOCKS proxy στην πόρτα %d; Ο διακομιστής μεσολάβησης (proxy server) πρέπει να είναι ενεργός πριν ενεργοποιηθεί αυτή η ρύθμιση.</string>
<string name="smp_servers_add">Προσθήκη διακομιστή…</string>
<string name="network_settings">Προχωρημένες ρυθμίσεις δικτύου</string>
<string name="v4_3_improved_server_configuration_desc">Προσθήκη διακομιστών μέσω σάρωσης QR κωδικών.</string>
@@ -298,7 +298,7 @@
<string name="icon_descr_cancel_live_message">Cancelar mensaje en directo</string>
<string name="confirm_verb">Confirmar</string>
<string name="clear_chat_menu_action">Vaciar</string>
<string name="app_version_code">Build de la aplicación</string>
<string name="app_version_code">Build de la aplicación: %s</string>
<string name="call_already_ended">¡La llamada ha terminado!</string>
<string name="rcv_conn_event_switch_queue_phase_completed">el servidor de envío ha cambiado para tí</string>
<string name="icon_descr_cancel_link_preview">cancelar vista previa del enlace</string>
@@ -171,7 +171,6 @@
<string name="chat_archive_header">Arkisto</string>
<string name="delete_chat_archive_question">Poista keskusteluarkisto\?</string>
<string name="archive_created_on_ts">Luotu %1$s</string>
<string name="rcv_group_event_changed_your_role">%s:n rooli muutettu %s:ksi</string>
<string name="rcv_group_event_group_deleted">poistettu ryhmä</string>
<string name="group_member_status_connecting">yhdistää</string>
<string name="group_member_status_accepted">yhdistäminen (hyväksytty)</string>
@@ -687,7 +687,6 @@
<string name="sender_cancelled_file_transfer">ファイル送信が中止されました。</string>
<string name="sender_may_have_deleted_the_connection_request">送信元が繋がりリクエストを削除したかもしれません。</string>
<string name="error_smp_test_server_auth">このサーバで待ち行列を作るには認証が必要です。パスワードをご確認ください。</string>
<string name="periodic_notifications_desc">アプリが定期的に新しいメッセージを受信します。一日の電池使用量が約3%で、プッシュ通知に頼らずに、あなたの端末のデータをサーバに送ることはありません。</string>
<string name="la_notice_title_simplex_lock">SimpleXロック</string>
<string name="enter_passphrase_notification_desc">通知を受けるには、データベースの暗証フレーズを入力してください。</string>
<string name="simplex_service_notification_title">SimpleX Chat サービス</string>
@@ -904,7 +903,6 @@
<string name="settings_section_title_support">SIMPLEX CHATを支援</string>
<string name="smp_servers_test_servers">テストサーバ</string>
<string name="switch_receiving_address_desc">受信アドレスは別のサーバーに変更されます。アドレス変更は送信者がオンラインになった後に完了します。</string>
<string name="to_preserve_privacy_simplex_has_background_service_instead_of_push_notifications_it_uses_a_few_pc_battery"><![CDATA[あなたのプライバシーを守るために、このアプリはプッシュ通知の変わりに <b>SimpleX バックグラウンド・サービス</b> を使ってます。一日の電池使用量は約3%です。]]></string>
<string name="to_protect_privacy_simplex_has_ids_for_queues">あなたのプライバシーを守るために、他のアプリと違って、ユーザーIDの変わりに SimpleX メッセージ束毎にIDを配布し、各連絡先が別々と扱います。</string>
<string name="group_main_profile_sent">あなたのチャットプロフィールが他のグループメンバーに送られます。</string>
<string name="to_verify_compare">エンドツーエンド暗号化を確認するには、ご自分の端末と連絡先の端末のコードを比べます (スキャンします)。</string>
@@ -337,7 +337,7 @@
<string name="error_sending_message">Mesaj gönderilirken hata oluştu</string>
<string name="error_creating_address">Adres oluştururken hata oluştu</string>
<string name="error_changing_address">Adres değiştirirken hata oluştu</string>
<string name="contact_wants_to_connect_via_call">1$s sizinle şu yolla bağlantı kurmak istiyor</string>
<string name="contact_wants_to_connect_via_call">%1$s sizinle şu yolla bağlantı kurmak istiyor</string>
<string name="error_changing_message_deletion">Ayarları değiştirirken hata oluştu</string>
<string name="error_creating_link_for_group">Toplu konuşma bağlantısı oluştururken hata oluştu</string>
<string name="error_changing_role">Yetki değiştirirken hata oluştu</string>
@@ -747,9 +747,9 @@
<string name="impossible_to_recover_passphrase"><![CDATA[<b>Aklınızda bulunsun</b>: kaybederseniz, parolayı kurtaramaz veya değiştiremezsiniz.]]></string>
<string name="chat_archive_header">Sohbet arşivi</string>
<string name="chat_archive_section">SOHBET ARŞİVİ</string>
<string name="group_invitation_item_description">1$s grubuna davet</string>
<string name="group_invitation_item_description">%1$s grubuna davet</string>
<string name="join_group_question">Gruba katıl\?</string>
<string name="rcv_group_event_member_added">1$s davet edildi</string>
<string name="rcv_group_event_member_added">%1$s davet edildi</string>
<string name="rcv_group_event_invited_via_your_group_link">grup bağlantınız üzerinden davet edildi</string>
<string name="group_member_status_invited">davet edildi</string>
<string name="invite_to_group_button">Gruba davet edin</string>
@@ -1344,7 +1344,7 @@
<string name="v5_2_message_delivery_receipts_descr">我们错过的第二个\"√\"!✅</string>
<string name="setup_database_passphrase">设定数据库密码</string>
<string name="receipts_groups_title_disable">为群组禁用回执吗?</string>
<string name="rcv_group_event_3_members_connected">%s、%s 和 %d 已连接</string>
<string name="rcv_group_event_3_members_connected">%s、%s 和 %s 已连接</string>
<string name="fix_connection_not_supported_by_group_member">修复群组成员不支持的问题</string>
<string name="receipts_groups_override_enabled">已为 %d 组启用送达回执功能</string>
<string name="sync_connection_force_confirm">重新协商</string>
@@ -1427,7 +1427,6 @@
<string name="connect_plan_connect_via_link">通过链接进行连接吗?</string>
<string name="connect_plan_already_joining_the_group">已经加入了该群组!</string>
<string name="group_members_n">%s、 %s 和 %d 名成员</string>
<string name="moderated_items_description">%s 审核了 %d 条消息</string>
<string name="unblock_member_button">解封成员</string>
<string name="connect_plan_connect_to_yourself">连接到你自己?</string>
<string name="contact_tap_to_connect">轻按连接</string>
@@ -11,7 +11,7 @@
<string name="about_simplex_chat">關於 SimpleX Chat</string>
<string name="accept_connection_request__question">接受連接請求?</string>
<string name="callstatus_accepted">已接受通話</string>
<string name="network_enable_socks_info">要在端口啟用 SOCKS 代理伺服器嗎?在啟用這個選項之前,必須先啟用代理伺服器。</string>
<string name="network_enable_socks_info">要在端口啟用 SOCKS 代理伺服器嗎 %d?在啟用這個選項之前,必須先啟用代理伺服器。</string>
<string name="group_member_role_admin">管理員</string>
<string name="above_then_preposition_continuation">然後,選按:</string>
<string name="smp_servers_preset_add">新增預設伺服器</string>
@@ -5,11 +5,11 @@ import androidx.compose.ui.text.*
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.Density
import chat.simplex.common.model.*
import chat.simplex.common.model.CIFile
import chat.simplex.common.model.readCryptoFile
import chat.simplex.common.platform.*
import chat.simplex.common.simplexWindowState
import java.io.ByteArrayInputStream
import java.io.File
import java.io.*
import java.net.URI
import javax.imageio.ImageIO
import kotlin.io.encoding.Base64
@@ -148,9 +148,8 @@ actual suspend fun saveTempImageUncompressed(image: ImageBitmap, asPng: Boolean)
return if (file != null) {
try {
val ext = if (asPng) "png" else "jpg"
val newFile = File(file.absolutePath + File.separator + generateNewFileName("IMG", ext, File(getAppFilePath(""))))
// LALAL FILE IS EMPTY
ImageIO.write(image.toAwtImage(), ext.uppercase(), newFile.outputStream())
val newFile = File(file.absolutePath + File.separator + generateNewFileName("IMG", ext, File(file.absolutePath)))
ImageIO.write(image.toAwtImage(), ext, newFile.outputStream())
newFile
} catch (e: Exception) {
Log.e(TAG, "Util.kt saveTempImageUncompressed error: ${e.message}")
+1 -1
View File
@@ -14,7 +14,7 @@ constraints: zip +disable-bzip2 +disable-zstd
source-repository-package
type: git
location: https://github.com/simplex-chat/simplexmq.git
tag: 18be2709f59a4cb20fe9758b899622092dba062e
tag: 8c250ebe19f56dd7d53572d984e8016cb0e4d658
source-repository-package
type: git
+1 -1
View File
@@ -45,7 +45,7 @@ dependencies:
- sqlcipher-simple == 0.4.*
- stm == 2.5.*
- terminal == 0.2.*
- time == 1.9.*
- time == 1.12.*
- tls >= 1.7.0 && < 1.8
- unliftio == 0.2.*
- unliftio-core == 0.2.*
+1 -1
View File
@@ -1,5 +1,5 @@
{
"https://github.com/simplex-chat/simplexmq.git"."18be2709f59a4cb20fe9758b899622092dba062e" = "08dr4vyg1wz2z768iikg8fks5zqf4dw5myr87hbpv964idda3pmj";
"https://github.com/simplex-chat/simplexmq.git"."8c250ebe19f56dd7d53572d984e8016cb0e4d658" = "080rw86yncf1h3zr5a8y65cndihq6f3ji43vxrdhr2mrb75vmw8m";
"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";
+7 -7
View File
@@ -200,7 +200,7 @@ library
, sqlcipher-simple ==0.4.*
, stm ==2.5.*
, terminal ==0.2.*
, time ==1.9.*
, time ==1.12.*
, tls >=1.7.0 && <1.8
, unliftio ==0.2.*
, unliftio-core ==0.2.*
@@ -260,7 +260,7 @@ executable simplex-bot
, sqlcipher-simple ==0.4.*
, stm ==2.5.*
, terminal ==0.2.*
, time ==1.9.*
, time ==1.12.*
, tls >=1.7.0 && <1.8
, unliftio ==0.2.*
, unliftio-core ==0.2.*
@@ -320,7 +320,7 @@ executable simplex-bot-advanced
, sqlcipher-simple ==0.4.*
, stm ==2.5.*
, terminal ==0.2.*
, time ==1.9.*
, time ==1.12.*
, tls >=1.7.0 && <1.8
, unliftio ==0.2.*
, unliftio-core ==0.2.*
@@ -382,7 +382,7 @@ executable simplex-broadcast-bot
, sqlcipher-simple ==0.4.*
, stm ==2.5.*
, terminal ==0.2.*
, time ==1.9.*
, time ==1.12.*
, tls >=1.7.0 && <1.8
, unliftio ==0.2.*
, unliftio-core ==0.2.*
@@ -443,7 +443,7 @@ executable simplex-chat
, sqlcipher-simple ==0.4.*
, stm ==2.5.*
, terminal ==0.2.*
, time ==1.9.*
, time ==1.12.*
, tls >=1.7.0 && <1.8
, unliftio ==0.2.*
, unliftio-core ==0.2.*
@@ -509,7 +509,7 @@ executable simplex-directory-service
, sqlcipher-simple ==0.4.*
, stm ==2.5.*
, terminal ==0.2.*
, time ==1.9.*
, time ==1.12.*
, tls >=1.7.0 && <1.8
, unliftio ==0.2.*
, unliftio-core ==0.2.*
@@ -603,7 +603,7 @@ test-suite simplex-chat-test
, sqlcipher-simple ==0.4.*
, stm ==2.5.*
, terminal ==0.2.*
, time ==1.9.*
, time ==1.12.*
, tls >=1.7.0 && <1.8
, unliftio ==0.2.*
, unliftio-core ==0.2.*
+75 -44
View File
@@ -35,7 +35,7 @@ import qualified Data.ByteString.Char8 as B
import qualified Data.ByteString.Lazy.Char8 as LB
import Data.Char
import Data.Constraint (Dict (..))
import Data.Either (fromRight, partitionEithers, rights)
import Data.Either (fromRight, lefts, partitionEithers, rights)
import Data.Fixed (div')
import Data.Functor (($>))
import Data.Int (Int64)
@@ -477,12 +477,14 @@ processChatCommand = \case
coupleDaysAgo t = (`addUTCTime` t) . fromInteger . negate . (+ (2 * day)) <$> randomRIO (0, day)
day = 86400
ListUsers -> CRUsersList <$> withStoreCtx' (Just "ListUsers, getUsersInfo") getUsersInfo
APISetActiveUser userId' viewPwd_ -> withUser $ \user -> do
APISetActiveUser userId' viewPwd_ -> do
unlessM chatStarted $ throwChatError CEChatNotStarted
user_ <- chatReadVar currentUser
user' <- privateGetUser userId'
validateUserPassword user user' viewPwd_
validateUserPassword_ user_ user' viewPwd_
withStoreCtx' (Just "APISetActiveUser, setActiveUser") $ \db -> setActiveUser db userId'
let user'' = user' {activeUser = True}
asks currentUser >>= atomically . (`writeTVar` Just user'')
chatWriteVar currentUser $ Just user''
pure $ CRActiveUser user''
SetActiveUser uName viewPwd_ -> do
tryChatError (withStore (`getUserIdByName` uName)) >>= \case
@@ -2262,11 +2264,14 @@ processChatCommand = \case
tryChatError (withStore (`getUser` userId)) >>= \case
Left _ -> throwChatError CEUserUnknown
Right user -> pure user
validateUserPassword :: User -> User -> Maybe UserPwd -> m ()
validateUserPassword User {userId} User {userId = userId', viewPwdHash} viewPwd_ =
validateUserPassword :: User -> User -> Maybe UserPwd -> m ()
validateUserPassword = validateUserPassword_ . Just
validateUserPassword_ :: Maybe User -> User -> Maybe UserPwd -> m ()
validateUserPassword_ user_ User {userId = userId', viewPwdHash} viewPwd_ =
forM_ viewPwdHash $ \pwdHash ->
let pwdOk = case viewPwd_ of
Nothing -> userId == userId'
let userId_ = (\User {userId} -> userId) <$> user_
pwdOk = case viewPwd_ of
Nothing -> userId_ == Just userId'
Just (UserPwd viewPwd) -> validPassword viewPwd pwdHash
in unless pwdOk $ throwChatError CEUserUnknown
validPassword :: Text -> UserPwdHash -> Bool
@@ -2289,16 +2294,16 @@ processChatCommand = \case
pure $ CRUserPrivacy {user, updatedUser = user'}
checkDeleteChatUser :: User -> m ()
checkDeleteChatUser user@User {userId} = do
when (activeUser user) $ throwChatError (CECantDeleteActiveUser userId)
users <- withStore' getUsers
unless (length users > 1 && (isJust (viewPwdHash user) || length (filter (isNothing . viewPwdHash) users) > 1)) $
throwChatError (CECantDeleteLastUser userId)
let otherVisible = filter (\User {userId = userId', viewPwdHash} -> userId /= userId' && isNothing viewPwdHash) users
when (activeUser user && length otherVisible > 0) $ throwChatError (CECantDeleteActiveUser userId)
deleteChatUser :: User -> Bool -> m ChatResponse
deleteChatUser user delSMPQueues = do
filesInfo <- withStore' (`getUserFileInfo` user)
forM_ filesInfo $ \fileInfo -> deleteFile user fileInfo
withAgent $ \a -> deleteUser a (aUserId user) delSMPQueues
withStore' (`deleteUserRecord` user)
when (activeUser user) $ chatWriteVar currentUser Nothing
ok_
updateChatSettings :: ChatName -> (ChatSettings -> ChatSettings) -> m ChatResponse
updateChatSettings (ChatName cType name) updateSettings = withUser $ \user -> do
@@ -5086,7 +5091,7 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
Left _ -> messageError "x.grp.mem.inv error: referenced member does not exist"
Right reMember -> do
GroupMemberIntro {introId} <- withStore $ \db -> saveIntroInvitation db reMember m introInv
void . sendGroupMessage' user [reMember] (XGrpMemFwd (memberInfo m) introInv) groupId (Just introId) $
sendGroupMemberMessage user reMember (XGrpMemFwd (memberInfo m) introInv) groupId (Just introId) $
withStore' $
\db -> updateIntroStatus db introId GMIntroInvForwarded
_ -> messageError "x.grp.mem.inv can be only sent by invitee member"
@@ -5680,23 +5685,34 @@ deliverMessage conn cmEventTag msgBody msgId = do
deliverMessage' conn msgFlags msgBody msgId
deliverMessage' :: ChatMonad m => Connection -> MsgFlags -> MsgBody -> MessageId -> m Int64
deliverMessage' conn@Connection {connId} msgFlags msgBody msgId = do
agentMsgId <- withAgent $ \a -> sendMessage a (aConnId conn) msgFlags msgBody
let sndMsgDelivery = SndMsgDelivery {connId, agentMsgId}
withStore' $ \db -> createSndMsgDelivery db sndMsgDelivery msgId
deliverMessage' conn msgFlags msgBody msgId =
deliverMessages [(conn, msgFlags, msgBody, msgId)] >>= \case
[r] -> liftEither r
rs -> throwChatError $ CEInternalError $ "deliverMessage: expected 1 result, got " <> show (length rs)
deliverMessages :: ChatMonad' m => [(Connection, MsgFlags, MsgBody, MessageId)] -> m [Either ChatError Int64]
deliverMessages msgReqs = do
sent <- zipWith prepareBatch msgReqs <$> withAgent' (`sendMessages` aReqs)
withStoreBatch $ \db -> map (bindRight $ createDelivery db) sent
where
aReqs = map (\(conn, msgFlags, msgBody, _msgId) -> (aConnId conn, msgFlags, msgBody)) msgReqs
prepareBatch req = bimap (`ChatErrorAgent` Nothing) (req,)
createDelivery :: DB.Connection -> ((Connection, MsgFlags, MsgBody, MessageId), AgentMsgId) -> IO (Either ChatError Int64)
createDelivery db ((Connection {connId}, _, _, msgId), agentMsgId) =
Right <$> createSndMsgDelivery db (SndMsgDelivery {connId, agentMsgId}) msgId
sendGroupMessage :: (MsgEncodingI e, ChatMonad m) => User -> GroupInfo -> [GroupMember] -> ChatMsgEvent e -> m (SndMessage, [GroupMember])
sendGroupMessage user GroupInfo {groupId} members chatMsgEvent =
sendGroupMessage' user members chatMsgEvent groupId Nothing $ pure ()
sendGroupMessage' :: forall e m. (MsgEncodingI e, ChatMonad m) => User -> [GroupMember] -> ChatMsgEvent e -> Int64 -> Maybe Int64 -> m () -> m (SndMessage, [GroupMember])
sendGroupMessage' user members chatMsgEvent groupId introId_ postDeliver = do
msg <- createSndMessage chatMsgEvent (GroupId groupId)
-- TODO collect failed deliveries into a single error
sendGroupMessage user GroupInfo {groupId} members chatMsgEvent = do
msg@SndMessage {msgId, msgBody} <- createSndMessage chatMsgEvent (GroupId groupId)
recipientMembers <- liftIO $ shuffleMembers (filter memberCurrent members)
rs <- forM recipientMembers $ \m ->
messageMember m msg `catchChatError` (\e -> toView (CRChatError (Just user) e) $> Nothing)
let sentToMembers = catMaybes rs
let msgFlags = MsgFlags {notification = hasNotification $ toCMEventTag chatMsgEvent}
(toSend, pending) = foldr addMember ([], []) recipientMembers
msgReqs = map (\(_, conn) -> (conn, msgFlags, msgBody, msgId)) toSend
delivered <- deliverMessages msgReqs
let errors = lefts delivered
unless (null errors) $ toView $ CRChatErrors (Just user) errors
stored <- withStoreBatch' $ \db -> map (\m -> createPendingGroupMessage db (groupMemberId' m) msgId Nothing) pending
let sentToMembers = filterSent delivered toSend fst <> filterSent stored pending id
pure (msg, sentToMembers)
where
shuffleMembers :: [GroupMember] -> IO [GroupMember]
@@ -5705,26 +5721,31 @@ sendGroupMessage' user members chatMsgEvent groupId introId_ postDeliver = do
liftM2 (<>) (shuffle adminMs) (shuffle otherMs)
where
isAdmin GroupMember {memberRole} = memberRole >= GRAdmin
messageMember :: GroupMember -> SndMessage -> m (Maybe GroupMember)
messageMember m@GroupMember {groupMemberId} SndMessage {msgId, msgBody} = case memberConn m of
Nothing -> pendingOrForwarded
Just conn@Connection {connStatus}
| connDisabled conn || connStatus == ConnDeleted -> pure Nothing
| connStatus == ConnSndReady || connStatus == ConnReady -> do
let tag = toCMEventTag chatMsgEvent
deliverMessage conn tag msgBody msgId >> postDeliver
pure $ Just m
| otherwise -> pendingOrForwarded
addMember m (toSend, pending) = case memberSendAction chatMsgEvent members m of
Just (MSASend conn) -> ((m, conn) : toSend, pending)
Just MSAPending -> (toSend, m : pending)
Nothing -> (toSend, pending)
filterSent :: [Either ChatError a] -> [mem] -> (mem -> GroupMember) -> [GroupMember]
filterSent rs ms mem = [mem m | (Right _, m) <- zip rs ms]
data MemberSendAction = MSASend Connection | MSAPending
memberSendAction :: ChatMsgEvent e -> [GroupMember] -> GroupMember -> Maybe MemberSendAction
memberSendAction chatMsgEvent members m = case memberConn m of
Nothing -> pendingOrForwarded
Just conn@Connection {connStatus}
| connDisabled conn || connStatus == ConnDeleted -> Nothing
| connStatus == ConnSndReady || connStatus == ConnReady -> Just (MSASend conn)
| otherwise -> pendingOrForwarded
where
pendingOrForwarded
| forwardSupported && isForwardedGroupMsg chatMsgEvent = Nothing
| isXGrpMsgForward chatMsgEvent = Nothing
| otherwise = Just MSAPending
where
pendingOrForwarded
| forwardSupported && isForwardedGroupMsg chatMsgEvent = pure Nothing
| isXGrpMsgForward chatMsgEvent = pure Nothing
| otherwise = do
withStore' $ \db -> createPendingGroupMessage db groupMemberId msgId introId_
pure $ Just m
forwardSupported = do
forwardSupported =
let mcvr = memberChatVRange' m
isCompatibleRange mcvr groupForwardVRange && invitingMemberSupportsForward
in isCompatibleRange mcvr groupForwardVRange && invitingMemberSupportsForward
invitingMemberSupportsForward = case m.invitedByGroupMemberId of
Just invMemberId ->
-- can be optimized for large groups by replacing [GroupMember] with Map GroupMemberId GroupMember
@@ -5738,6 +5759,16 @@ sendGroupMessage' user members chatMsgEvent groupId introId_ postDeliver = do
XGrpMsgForward {} -> True
_ -> False
sendGroupMemberMessage :: forall e m. (MsgEncodingI e, ChatMonad m) => User -> GroupMember -> ChatMsgEvent e -> Int64 -> Maybe Int64 -> m () -> m ()
sendGroupMemberMessage user m@GroupMember {groupMemberId} chatMsgEvent groupId introId_ postDeliver = do
msg <- createSndMessage chatMsgEvent (GroupId groupId)
messageMember msg `catchChatError` (\e -> toView (CRChatError (Just user) e))
where
messageMember :: SndMessage -> m ()
messageMember SndMessage {msgId, msgBody} = forM_ (memberSendAction chatMsgEvent [m] m) $ \case
MSASend conn -> deliverMessage conn (toCMEventTag chatMsgEvent) msgBody msgId >> postDeliver
MSAPending -> withStore' $ \db -> createPendingGroupMessage db groupMemberId msgId introId_
sendPendingGroupMessages :: ChatMonad m => User -> GroupMember -> Connection -> m ()
sendPendingGroupMessages user GroupMember {groupMemberId, localDisplayName} conn = do
pendingMessages <- withStore' $ \db -> getPendingGroupMessages db groupMemberId
+15
View File
@@ -84,6 +84,7 @@ import Simplex.RemoteControl.Invitation (RCSignedInvitation, RCVerifiedInvitatio
import Simplex.RemoteControl.Types
import System.IO (Handle)
import System.Mem.Weak (Weak)
import qualified UnliftIO.Exception as E
import UnliftIO.STM
versionNumber :: String
@@ -1288,12 +1289,26 @@ withStoreCtx ctx_ action = do
handleInternal :: String -> SomeException -> IO (Either StoreError a)
handleInternal ctxStr e = pure . Left . SEInternalError $ show e <> ctxStr
withStoreBatch :: (ChatMonad' m, Traversable t) => (DB.Connection -> t (IO (Either ChatError a))) -> m (t (Either ChatError a))
withStoreBatch actions = do
ChatController {chatStore} <- ask
liftIO $ withTransaction chatStore $ mapM (`E.catch` handleInternal) . actions
where
handleInternal :: E.SomeException -> IO (Either ChatError a)
handleInternal = pure . Left . ChatError . CEInternalError . show
withStoreBatch' :: (ChatMonad' m, Traversable t) => (DB.Connection -> t (IO a)) -> m (t (Either ChatError a))
withStoreBatch' actions = withStoreBatch $ fmap (fmap Right) . actions
withAgent :: ChatMonad m => (AgentClient -> ExceptT AgentErrorType m a) -> m a
withAgent action =
asks smpAgent
>>= runExceptT . action
>>= liftEither . first (`ChatErrorAgent` Nothing)
withAgent' :: ChatMonad' m => (AgentClient -> m a) -> m a
withAgent' action = asks smpAgent >>= action
$(JQ.deriveJSON (enumJSON $ dropPrefix "HS") ''HelpSection)
$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "CLQ") ''ChatListQuery)
+3 -1
View File
@@ -474,7 +474,9 @@ chatItemDeletedText ChatItem {meta = CIMeta {itemDeleted}, content} membership_
_ -> ""
viewUsersList :: [UserInfo] -> [StyledString]
viewUsersList = mapMaybe userInfo . sortOn ldn
viewUsersList us =
let ss = mapMaybe userInfo $ sortOn ldn us
in if null ss then ["no users"] else ss
where
ldn (UserInfo User {localDisplayName = n} _) = T.toLower n
userInfo (UserInfo User {localDisplayName = n, profile = LocalProfile {fullName}, activeUser, showNtfs, viewPwdHash} count)
+3 -2
View File
@@ -18,7 +18,7 @@ import Control.Monad.Except
import Data.ByteArray (ScrubbedBytes)
import Data.Functor (($>))
import Data.List (dropWhileEnd, find)
import Data.Maybe (fromJust, isNothing)
import Data.Maybe (isNothing)
import qualified Data.Text as T
import Network.Socket
import Simplex.Chat
@@ -284,7 +284,7 @@ getTermLine cc =
_ -> error "no output for 5 seconds"
userName :: TestCC -> IO [Char]
userName (TestCC ChatController {currentUser} _ _ _ _ _) = T.unpack . localDisplayName . fromJust <$> readTVarIO currentUser
userName (TestCC ChatController {currentUser} _ _ _ _ _) = maybe "no current user" (T.unpack . localDisplayName) <$> readTVarIO currentUser
testChat2 :: HasCallStack => Profile -> Profile -> (HasCallStack => TestCC -> TestCC -> IO ()) -> FilePath -> IO ()
testChat2 = testChatCfgOpts2 testCfg testOpts
@@ -353,6 +353,7 @@ serverCfg =
serverStatsBackupFile = Nothing,
smpServerVRange = supportedSMPServerVRange,
transportConfig = defaultTransportServerConfig,
smpHandshakeTimeout = 1000000,
controlPort = Nothing
}
+41 -26
View File
@@ -1492,16 +1492,16 @@ testDeleteUser =
\alice bob cath dan -> do
connectUsers alice bob
-- cannot delete active user
alice ##> "/create user alisa"
showActiveUser alice "alisa"
alice ##> "/_delete user 1 del_smp=off"
-- cannot delete active user when there is another user
alice ##> "/_delete user 2 del_smp=off"
alice <## "cannot delete active user"
-- delete user without deleting SMP queues
alice ##> "/create user alisa"
showActiveUser alice "alisa"
connectUsers alice cath
alice <##> cath
@@ -1519,17 +1519,7 @@ testDeleteUser =
-- no connection authorization error - connection wasn't deleted
(alice </)
-- cannot delete new active user
alice ##> "/delete user alisa"
alice <## "cannot delete active user"
alice ##> "/users"
alice <## "alisa (active)"
alice <##> cath
-- delete user deleting SMP queues
-- cannot delete active user when there is another user
alice ##> "/create user alisa2"
showActiveUser alice "alisa2"
@@ -1537,10 +1527,17 @@ testDeleteUser =
connectUsers alice dan
alice <##> dan
alice ##> "/delete user alisa2"
alice <## "cannot delete active user"
alice ##> "/users"
alice <## "alisa"
alice <## "alisa2 (active)"
alice <##> dan
-- delete user deleting SMP queues
alice ##> "/delete user alisa"
alice <### ["ok", "completed deleting user"]
@@ -1553,6 +1550,16 @@ testDeleteUser =
alice <##> dan
-- delete last active user
alice ##> "/delete user alisa2 del_smp=off"
alice <### ["ok", "completed deleting user"]
alice ##> "/users"
alice <## "no users"
alice ##> "/create user alisa3"
showActiveUser alice "alisa3"
testUsersDifferentCIExpirationTTL :: HasCallStack => FilePath -> IO ()
testUsersDifferentCIExpirationTTL tmp = do
withNewTestChat tmp "bob" bobProfile $ \bob -> do
@@ -2047,12 +2054,23 @@ testUserPrivacy =
userVisible alice "current "
alice ##> "/hide user new_password"
userHidden alice "current "
alice ##> "/_delete user 1 del_smp=on"
alice <## "cannot delete last user"
alice ##> "/_hide user 1 \"password\""
alice <## "cannot hide the only not hidden user"
alice ##> "/user alice"
showActiveUser alice "alice (Alice)"
-- delete last visible active user
alice ##> "/_delete user 1 del_smp=on"
alice <### ["ok", "completed deleting user"]
-- hidden user is not shown
alice ##> "/users"
alice <## "no users"
-- but it is still possible to switch to it
alice ##> "/user alisa wrong_password"
alice <## "user does not exist or incorrect password"
alice ##> "/user alisa new_password"
showActiveUser alice "alisa"
alice ##> "/create user alisa2"
showActiveUser alice "alisa2"
alice ##> "/_hide user 3 \"password2\""
alice <## "cannot hide the only not hidden user"
-- change profile privacy for inactive user via API requires correct password
alice ##> "/_unmute user 2"
alice <## "hidden user always muted when inactive"
@@ -2064,17 +2082,14 @@ testUserPrivacy =
userVisible alice ""
alice ##> "/_hide user 2 \"another_password\""
userHidden alice ""
alice ##> "/user alisa another_password"
showActiveUser alice "alisa"
alice ##> "/user alice"
showActiveUser alice "alice (Alice)"
alice ##> "/_delete user 2 del_smp=on"
alice <## "user does not exist or incorrect password"
alice ##> "/_delete user 2 del_smp=on \"wrong_password\""
alice <## "user does not exist or incorrect password"
alice ##> "/_delete user 2 del_smp=on \"another_password\""
alice <## "ok"
alice <## "completed deleting user"
alice <### ["ok", "completed deleting user"]
alice ##> "/_delete user 3 del_smp=on"
alice <### ["ok", "completed deleting user"]
where
userHidden alice current = do
alice <## (current <> "user alisa:")