From aea7ff1c8980bb9c20944e004de05862419ceb96 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Wed, 13 Dec 2023 20:27:58 +0000 Subject: [PATCH 01/13] nix: fix script --- scripts/nix/update-sha256.awk | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/nix/update-sha256.awk b/scripts/nix/update-sha256.awk index e432ec32d..6ee0c9a15 100644 --- a/scripts/nix/update-sha256.awk +++ b/scripts/nix/update-sha256.awk @@ -11,7 +11,7 @@ BEGIN { /tag/ && isGit == true { ref=$2 } isGit == true && loc != "" && ref != "" { - cmd = "nix-prefetch-git --quiet "loc" "ref" | jq -r .sha256" + cmd = "nix-prefetch-git --fetch-submodules --quiet "loc" "ref" | jq -r .sha256" cmd | getline sha256 close(cmd) print " \""loc"\".\""ref"\" = \""sha256"\";"; From 73130bf3215aa37a8b2ec013a566d1cb650864e2 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Wed, 13 Dec 2023 21:48:25 +0000 Subject: [PATCH 02/13] ios: update core library --- apps/ios/SimpleX.xcodeproj/project.pbxproj | 40 +++++++++++----------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index 60a9e2ff0..f5701af15 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -116,11 +116,11 @@ 5CC2C0FF2809BF11000C35E3 /* SimpleX--iOS--InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 5CC2C0FD2809BF11000C35E3 /* SimpleX--iOS--InfoPlist.strings */; }; 5CC868F329EB540C0017BBFD /* CIRcvDecryptionError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CC868F229EB540C0017BBFD /* CIRcvDecryptionError.swift */; }; 5CCB939C297EFCB100399E78 /* NavStackCompat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CCB939B297EFCB100399E78 /* NavStackCompat.swift */; }; - 5CCD1A602B27927E001A4199 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CCD1A5B2B27927E001A4199 /* libgmpxx.a */; }; - 5CCD1A612B27927E001A4199 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CCD1A5C2B27927E001A4199 /* libgmp.a */; }; - 5CCD1A622B27927E001A4199 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CCD1A5D2B27927E001A4199 /* libffi.a */; }; - 5CCD1A632B27927E001A4199 /* libHSsimplex-chat-5.4.0.7-EoJ0xKOyE47DlSpHXf0V4.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CCD1A5E2B27927E001A4199 /* libHSsimplex-chat-5.4.0.7-EoJ0xKOyE47DlSpHXf0V4.a */; }; - 5CCD1A642B27927E001A4199 /* libHSsimplex-chat-5.4.0.7-EoJ0xKOyE47DlSpHXf0V4-ghc8.10.7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CCD1A5F2B27927E001A4199 /* libHSsimplex-chat-5.4.0.7-EoJ0xKOyE47DlSpHXf0V4-ghc8.10.7.a */; }; + 5CCD1A882B2A5D56001A4199 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CCD1A832B2A5D55001A4199 /* libgmp.a */; }; + 5CCD1A892B2A5D56001A4199 /* libHSsimplex-chat-5.4.0.7-8PiOsot1xukLpqHaIcecqn.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CCD1A842B2A5D55001A4199 /* libHSsimplex-chat-5.4.0.7-8PiOsot1xukLpqHaIcecqn.a */; }; + 5CCD1A8A2B2A5D56001A4199 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CCD1A852B2A5D55001A4199 /* libgmpxx.a */; }; + 5CCD1A8B2B2A5D56001A4199 /* libHSsimplex-chat-5.4.0.7-8PiOsot1xukLpqHaIcecqn-ghc8.10.7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CCD1A862B2A5D55001A4199 /* libHSsimplex-chat-5.4.0.7-8PiOsot1xukLpqHaIcecqn-ghc8.10.7.a */; }; + 5CCD1A8C2B2A5D56001A4199 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CCD1A872B2A5D56001A4199 /* libffi.a */; }; 5CCD403427A5F6DF00368C90 /* AddContactView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CCD403327A5F6DF00368C90 /* AddContactView.swift */; }; 5CCD403727A5F9A200368C90 /* ScanToConnectView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CCD403627A5F9A200368C90 /* ScanToConnectView.swift */; }; 5CD67B8F2B0E858A00C510B1 /* hs_init.h in Headers */ = {isa = PBXBuildFile; fileRef = 5CD67B8D2B0E858A00C510B1 /* hs_init.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -402,11 +402,11 @@ 5CC2C0FE2809BF11000C35E3 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = "ru.lproj/SimpleX--iOS--InfoPlist.strings"; sourceTree = ""; }; 5CC868F229EB540C0017BBFD /* CIRcvDecryptionError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIRcvDecryptionError.swift; sourceTree = ""; }; 5CCB939B297EFCB100399E78 /* NavStackCompat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavStackCompat.swift; sourceTree = ""; }; - 5CCD1A5B2B27927E001A4199 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = ""; }; - 5CCD1A5C2B27927E001A4199 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = ""; }; - 5CCD1A5D2B27927E001A4199 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; }; - 5CCD1A5E2B27927E001A4199 /* libHSsimplex-chat-5.4.0.7-EoJ0xKOyE47DlSpHXf0V4.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.4.0.7-EoJ0xKOyE47DlSpHXf0V4.a"; sourceTree = ""; }; - 5CCD1A5F2B27927E001A4199 /* libHSsimplex-chat-5.4.0.7-EoJ0xKOyE47DlSpHXf0V4-ghc8.10.7.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.4.0.7-EoJ0xKOyE47DlSpHXf0V4-ghc8.10.7.a"; sourceTree = ""; }; + 5CCD1A832B2A5D55001A4199 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = ""; }; + 5CCD1A842B2A5D55001A4199 /* libHSsimplex-chat-5.4.0.7-8PiOsot1xukLpqHaIcecqn.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.4.0.7-8PiOsot1xukLpqHaIcecqn.a"; sourceTree = ""; }; + 5CCD1A852B2A5D55001A4199 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = ""; }; + 5CCD1A862B2A5D55001A4199 /* libHSsimplex-chat-5.4.0.7-8PiOsot1xukLpqHaIcecqn-ghc8.10.7.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.4.0.7-8PiOsot1xukLpqHaIcecqn-ghc8.10.7.a"; sourceTree = ""; }; + 5CCD1A872B2A5D56001A4199 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; }; 5CCD403327A5F6DF00368C90 /* AddContactView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddContactView.swift; sourceTree = ""; }; 5CCD403627A5F9A200368C90 /* ScanToConnectView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScanToConnectView.swift; sourceTree = ""; }; 5CD67B8D2B0E858A00C510B1 /* hs_init.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = hs_init.h; sourceTree = ""; }; @@ -517,13 +517,13 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 5CCD1A602B27927E001A4199 /* libgmpxx.a in Frameworks */, 5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */, - 5CCD1A612B27927E001A4199 /* libgmp.a in Frameworks */, - 5CCD1A622B27927E001A4199 /* libffi.a in Frameworks */, + 5CCD1A8B2B2A5D56001A4199 /* libHSsimplex-chat-5.4.0.7-8PiOsot1xukLpqHaIcecqn-ghc8.10.7.a in Frameworks */, + 5CCD1A8A2B2A5D56001A4199 /* libgmpxx.a in Frameworks */, + 5CCD1A882B2A5D56001A4199 /* libgmp.a in Frameworks */, + 5CCD1A8C2B2A5D56001A4199 /* libffi.a in Frameworks */, + 5CCD1A892B2A5D56001A4199 /* libHSsimplex-chat-5.4.0.7-8PiOsot1xukLpqHaIcecqn.a in Frameworks */, 5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */, - 5CCD1A642B27927E001A4199 /* libHSsimplex-chat-5.4.0.7-EoJ0xKOyE47DlSpHXf0V4-ghc8.10.7.a in Frameworks */, - 5CCD1A632B27927E001A4199 /* libHSsimplex-chat-5.4.0.7-EoJ0xKOyE47DlSpHXf0V4.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -585,11 +585,11 @@ 5C764E5C279C70B7000C6508 /* Libraries */ = { isa = PBXGroup; children = ( - 5CCD1A5D2B27927E001A4199 /* libffi.a */, - 5CCD1A5C2B27927E001A4199 /* libgmp.a */, - 5CCD1A5B2B27927E001A4199 /* libgmpxx.a */, - 5CCD1A5F2B27927E001A4199 /* libHSsimplex-chat-5.4.0.7-EoJ0xKOyE47DlSpHXf0V4-ghc8.10.7.a */, - 5CCD1A5E2B27927E001A4199 /* libHSsimplex-chat-5.4.0.7-EoJ0xKOyE47DlSpHXf0V4.a */, + 5CCD1A872B2A5D56001A4199 /* libffi.a */, + 5CCD1A832B2A5D55001A4199 /* libgmp.a */, + 5CCD1A852B2A5D55001A4199 /* libgmpxx.a */, + 5CCD1A862B2A5D55001A4199 /* libHSsimplex-chat-5.4.0.7-8PiOsot1xukLpqHaIcecqn-ghc8.10.7.a */, + 5CCD1A842B2A5D55001A4199 /* libHSsimplex-chat-5.4.0.7-8PiOsot1xukLpqHaIcecqn.a */, ); path = Libraries; sourceTree = ""; From 8cec5428ee59cd84c54929524d28496801ff7851 Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Thu, 14 Dec 2023 17:08:40 +0400 Subject: [PATCH 03/13] core: save CIContent tag in chat_items table (#3555) --- simplex-chat.cabal | 1 + src/Simplex/Chat/Messages/CIContent.hs | 29 +++++++++++++++++++ .../Migrations/M20231214_item_content_tag.hs | 18 ++++++++++++ src/Simplex/Chat/Migrations/chat_schema.sql | 3 +- src/Simplex/Chat/Store/Messages.hs | 9 +++--- src/Simplex/Chat/Store/Migrations.hs | 4 ++- 6 files changed, 58 insertions(+), 6 deletions(-) create mode 100644 src/Simplex/Chat/Migrations/M20231214_item_content_tag.hs diff --git a/simplex-chat.cabal b/simplex-chat.cabal index d8c6f24fb..ce066bc3c 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -126,6 +126,7 @@ library Simplex.Chat.Migrations.M20231114_remote_control Simplex.Chat.Migrations.M20231126_remote_ctrl_address Simplex.Chat.Migrations.M20231207_chat_list_pagination + Simplex.Chat.Migrations.M20231214_item_content_tag Simplex.Chat.Mobile Simplex.Chat.Mobile.File Simplex.Chat.Mobile.Shared diff --git a/src/Simplex/Chat/Messages/CIContent.hs b/src/Simplex/Chat/Messages/CIContent.hs index 6b7e66bdb..a987603bb 100644 --- a/src/Simplex/Chat/Messages/CIContent.hs +++ b/src/Simplex/Chat/Messages/CIContent.hs @@ -574,3 +574,32 @@ dbParseACIContent = fmap aciContentDBJSON . J.eitherDecodeStrict' . encodeUtf8 -- platform specific instance FromJSON ACIContent where parseJSON = fmap aciContentJSON . J.parseJSON + +toCIContentTag :: CIContent e -> Text +toCIContentTag ciContent = case ciContent of + CISndMsgContent _ -> "sndMsgContent" + CIRcvMsgContent _ -> "rcvMsgContent" + CISndDeleted _ -> "sndDeleted" + CIRcvDeleted _ -> "rcvDeleted" + CISndCall {} -> "sndCall" + CIRcvCall {} -> "rcvCall" + CIRcvIntegrityError _ -> "rcvIntegrityError" + CIRcvDecryptionError {} -> "rcvDecryptionError" + CIRcvGroupInvitation {} -> "rcvGroupInvitation" + CISndGroupInvitation {} -> "sndGroupInvitation" + CIRcvDirectEvent _ -> "rcvDirectEvent" + CIRcvGroupEvent _ -> "rcvGroupEvent" + CISndGroupEvent _ -> "sndGroupEvent" + CIRcvConnEvent _ -> "rcvConnEvent" + CISndConnEvent _ -> "sndConnEvent" + CIRcvChatFeature {} -> "rcvChatFeature" + CISndChatFeature {} -> "sndChatFeature" + CIRcvChatPreference {} -> "rcvChatPreference" + CISndChatPreference {} -> "sndChatPreference" + CIRcvGroupFeature {} -> "rcvGroupFeature" + CISndGroupFeature {} -> "sndGroupFeature" + CIRcvChatFeatureRejected _ -> "rcvChatFeatureRejected" + CIRcvGroupFeatureRejected _ -> "rcvGroupFeatureRejected" + CISndModerated -> "sndModerated" + CIRcvModerated -> "rcvModerated" + CIInvalidJSON _ -> "invalidJSON" diff --git a/src/Simplex/Chat/Migrations/M20231214_item_content_tag.hs b/src/Simplex/Chat/Migrations/M20231214_item_content_tag.hs new file mode 100644 index 000000000..cd4cd136e --- /dev/null +++ b/src/Simplex/Chat/Migrations/M20231214_item_content_tag.hs @@ -0,0 +1,18 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Migrations.M20231214_item_content_tag where + +import Database.SQLite.Simple (Query) +import Database.SQLite.Simple.QQ (sql) + +m20231214_item_content_tag :: Query +m20231214_item_content_tag = + [sql| +ALTER TABLE chat_items ADD COLUMN item_content_tag TEXT; +|] + +down_m20231214_item_content_tag :: Query +down_m20231214_item_content_tag = + [sql| +ALTER TABLE chat_items DROP COLUMN item_content_tag; +|] diff --git a/src/Simplex/Chat/Migrations/chat_schema.sql b/src/Simplex/Chat/Migrations/chat_schema.sql index 3b83b132d..7f5945d39 100644 --- a/src/Simplex/Chat/Migrations/chat_schema.sql +++ b/src/Simplex/Chat/Migrations/chat_schema.sql @@ -379,7 +379,8 @@ CREATE TABLE chat_items( item_live INTEGER, item_deleted_by_group_member_id INTEGER REFERENCES group_members ON DELETE SET NULL, item_deleted_ts TEXT, - forwarded_by_group_member_id INTEGER REFERENCES group_members ON DELETE SET NULL + forwarded_by_group_member_id INTEGER REFERENCES group_members ON DELETE SET NULL, + item_content_tag TEXT ); CREATE TABLE chat_item_messages( chat_item_id INTEGER NOT NULL REFERENCES chat_items ON DELETE CASCADE, diff --git a/src/Simplex/Chat/Store/Messages.hs b/src/Simplex/Chat/Store/Messages.hs index 87e666712..b817c844d 100644 --- a/src/Simplex/Chat/Store/Messages.hs +++ b/src/Simplex/Chat/Store/Messages.hs @@ -399,18 +399,19 @@ createNewChatItem_ db User {userId} chatDirection msgId_ sharedMsgId ciContent q -- user and IDs user_id, created_by_msg_id, contact_id, group_id, group_member_id, -- meta - item_sent, item_ts, item_content, item_text, item_status, shared_msg_id, forwarded_by_group_member_id, created_at, updated_at, item_live, timed_ttl, timed_delete_at, + item_sent, item_ts, item_content, item_content_tag, item_text, item_status, shared_msg_id, + forwarded_by_group_member_id, created_at, updated_at, item_live, timed_ttl, timed_delete_at, -- quote quoted_shared_msg_id, quoted_sent_at, quoted_content, quoted_sent, quoted_member_id - ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) + ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) |] ((userId, msgId_) :. idsRow :. itemRow :. quoteRow) ciId <- insertedRowId db forM_ msgId_ $ \msgId -> insertChatItemMessage_ db ciId msgId createdAt pure ciId where - itemRow :: (SMsgDirection d, UTCTime, CIContent d, Text, CIStatus d, Maybe SharedMsgId, Maybe GroupMemberId) :. (UTCTime, UTCTime, Maybe Bool) :. (Maybe Int, Maybe UTCTime) - itemRow = (msgDirection @d, itemTs, ciContent, ciContentToText ciContent, ciCreateStatus ciContent, sharedMsgId, forwardedByMember) :. (createdAt, createdAt, justTrue live) :. ciTimedRow timed + itemRow :: (SMsgDirection d, UTCTime, CIContent d, Text, Text, CIStatus d, Maybe SharedMsgId, Maybe GroupMemberId) :. (UTCTime, UTCTime, Maybe Bool) :. (Maybe Int, Maybe UTCTime) + itemRow = (msgDirection @d, itemTs, ciContent, toCIContentTag ciContent, ciContentToText ciContent, ciCreateStatus ciContent, sharedMsgId, forwardedByMember) :. (createdAt, createdAt, justTrue live) :. ciTimedRow timed idsRow :: (Maybe Int64, Maybe Int64, Maybe Int64) idsRow = case chatDirection of CDDirectRcv Contact {contactId} -> (Just contactId, Nothing, Nothing) diff --git a/src/Simplex/Chat/Store/Migrations.hs b/src/Simplex/Chat/Store/Migrations.hs index c8a04c42a..af9985b83 100644 --- a/src/Simplex/Chat/Store/Migrations.hs +++ b/src/Simplex/Chat/Store/Migrations.hs @@ -92,6 +92,7 @@ import Simplex.Chat.Migrations.M20231113_group_forward import Simplex.Chat.Migrations.M20231114_remote_control import Simplex.Chat.Migrations.M20231126_remote_ctrl_address import Simplex.Chat.Migrations.M20231207_chat_list_pagination +import Simplex.Chat.Migrations.M20231214_item_content_tag import Simplex.Messaging.Agent.Store.SQLite.Migrations (Migration (..)) schemaMigrations :: [(String, Query, Maybe Query)] @@ -183,7 +184,8 @@ schemaMigrations = ("20231113_group_forward", m20231113_group_forward, Just down_m20231113_group_forward), ("20231114_remote_control", m20231114_remote_control, Just down_m20231114_remote_control), ("20231126_remote_ctrl_address", m20231126_remote_ctrl_address, Just down_m20231126_remote_ctrl_address), - ("20231207_chat_list_pagination", m20231207_chat_list_pagination, Just down_m20231207_chat_list_pagination) + ("20231207_chat_list_pagination", m20231207_chat_list_pagination, Just down_m20231207_chat_list_pagination), + ("20231214_item_content_tag", m20231214_item_content_tag, Just down_m20231214_item_content_tag) ] -- | The list of migrations in ascending order by date From 974fa448b4ec52387ca9e062734d3378d1967266 Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Thu, 14 Dec 2023 21:11:19 +0800 Subject: [PATCH 04/13] android, desktop: some alerts became privacy sensitive (#3554) * android, desktop: some alerts became privacy sensitive * changes --- .../java/chat/simplex/app/MainActivity.kt | 4 +- .../kotlin/chat/simplex/common/App.kt | 21 ++++++-- .../chat/simplex/common/model/ChatModel.kt | 4 +- .../chat/simplex/common/model/SimpleXAPI.kt | 3 +- .../simplex/common/views/chat/ComposeView.kt | 2 +- .../views/chatlist/ChatListNavLinkView.kt | 10 ++-- .../common/views/chatlist/ChatListView.kt | 9 +--- .../common/views/helpers/AlertManager.kt | 9 +++- .../common/views/localauth/LocalAuthView.kt | 3 +- .../common/views/newchat/ScanToConnectView.kt | 50 +++++++++---------- 10 files changed, 67 insertions(+), 48 deletions(-) diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/MainActivity.kt b/apps/multiplatform/android/src/main/java/chat/simplex/app/MainActivity.kt index cbe0ef7b1..082c10582 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/MainActivity.kt +++ b/apps/multiplatform/android/src/main/java/chat/simplex/app/MainActivity.kt @@ -124,7 +124,9 @@ fun processIntent(intent: Intent?) { when (intent?.action) { "android.intent.action.VIEW" -> { val uri = intent.data - if (uri != null) connectIfOpenedViaUri(chatModel.remoteHostId(), uri.toURI(), ChatModel) + if (uri != null) { + chatModel.appOpenUrl.value = null to uri.toURI() + } } } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/App.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/App.kt index 4387adf95..0082972c7 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/App.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/App.kt @@ -162,11 +162,26 @@ fun MainScreen() { AuthView() } else { SplashView() + ModalManager.fullscreen.showPasscodeInView() + } + } else { + if (chatModel.showCallView.value) { + ActiveCallView() + } else { + // It's needed for privacy settings toggle, so it can be shown even if the app is passcode unlocked + ModalManager.fullscreen.showPasscodeInView() + } + AlertManager.privacySensitive.showInView() + if (onboarding == OnboardingStage.OnboardingComplete) { + LaunchedEffect(chatModel.currentUser.value, chatModel.appOpenUrl.value) { + val (rhId, url) = chatModel.appOpenUrl.value ?: (null to null) + if (url != null) { + chatModel.appOpenUrl.value = null + connectIfOpenedViaUri(rhId, url, chatModel) + } + } } - } else if (chatModel.showCallView.value) { - ActiveCallView() } - ModalManager.fullscreen.showPasscodeInView() val invitation = chatModel.activeCallInvitation.value if (invitation != null) IncomingCallAlertView(invitation, chatModel) AlertManager.shared.showInView() diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt index 25c87d64a..2305862b6 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt @@ -70,8 +70,8 @@ object ChatModel { // Only needed during onboarding when user skipped password setup (left as random password) val desktopOnboardingRandomPassword = mutableStateOf(false) - // set when app is opened via contact or invitation URI - val appOpenUrl = mutableStateOf(null) + // set when app is opened via contact or invitation URI (rhId, uri) + val appOpenUrl = mutableStateOf?>(null) // preferences val notificationPreviewMode by lazy { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt index ad897c60f..4af3e3f2e 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt @@ -2023,7 +2023,8 @@ object ChatController { chatModel.chatId.value = null ModalManager.center.closeModals() ModalManager.end.closeModals() - AlertManager.shared.alertViews.clear() + AlertManager.shared.hideAllAlerts() + AlertManager.privacySensitive.hideAllAlerts() chatModel.currentRemoteHost.value = switchRemoteHost(rhId) reloadRemoteHosts() val user = apiGetActiveUser(rhId) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt index ddcfcf594..b230d261f 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt @@ -201,7 +201,7 @@ suspend fun MutableState.processPickedMedia(uris: List, text: // Image val drawable = getDrawableFromUri(uri) // Do not show alert in case it's already shown from the function above - bitmap = getBitmapFromUri(uri, withAlertOnException = AlertManager.shared.alertViews.isEmpty()) + bitmap = getBitmapFromUri(uri, withAlertOnException = !AlertManager.shared.hasAlertsShown()) if (isAnimImage(uri, drawable)) { // It's a gif or webp val fileSize = getFileSize(uri) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt index 9d662758f..9ae0da2a3 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt @@ -611,12 +611,12 @@ fun askCurrentOrIncognitoProfileConnectContactViaAddress( close: (() -> Unit)?, openChat: Boolean ) { - AlertManager.shared.showAlertDialogButtonsColumn( + AlertManager.privacySensitive.showAlertDialogButtonsColumn( title = String.format(generalGetString(MR.strings.connect_with_contact_name_question), contact.chatViewName), buttons = { Column { SectionItemView({ - AlertManager.shared.hideAlert() + AlertManager.privacySensitive.hideAlert() withApi { close?.invoke() val ok = connectContactViaAddress(chatModel, rhId, contact.contactId, incognito = false) @@ -628,7 +628,7 @@ fun askCurrentOrIncognitoProfileConnectContactViaAddress( Text(generalGetString(MR.strings.connect_use_current_profile), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) } SectionItemView({ - AlertManager.shared.hideAlert() + AlertManager.privacySensitive.hideAlert() withApi { close?.invoke() val ok = connectContactViaAddress(chatModel, rhId, contact.contactId, incognito = true) @@ -640,7 +640,7 @@ fun askCurrentOrIncognitoProfileConnectContactViaAddress( Text(generalGetString(MR.strings.connect_use_new_incognito_profile), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) } SectionItemView({ - AlertManager.shared.hideAlert() + AlertManager.privacySensitive.hideAlert() }) { Text(stringResource(MR.strings.cancel_verb), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) } @@ -654,7 +654,7 @@ suspend fun connectContactViaAddress(chatModel: ChatModel, rhId: Long?, contactI val contact = chatModel.controller.apiConnectContactViaAddress(rhId, incognito, contactId) if (contact != null) { chatModel.updateContact(rhId, contact) - AlertManager.shared.showAlertMsg( + AlertManager.privacySensitive.showAlertMsg( title = generalGetString(MR.strings.connection_request_sent), text = generalGetString(MR.strings.you_will_be_connected_when_your_connection_request_is_accepted), hostDevice = hostDevice(rhId), diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt index a91e5e7b3..18252d0e2 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt @@ -49,13 +49,6 @@ fun ChatListView(chatModel: ChatModel, settingsState: SettingsViewState, setPerf LaunchedEffect(chatModel.clearOverlays.value) { if (chatModel.clearOverlays.value && newChatSheetState.value.isVisible()) hideNewChatSheet(false) } - LaunchedEffect(chatModel.appOpenUrl.value) { - val url = chatModel.appOpenUrl.value - if (url != null) { - chatModel.appOpenUrl.value = null - connectIfOpenedViaUri(chatModel.remoteHostId(), url, chatModel) - } - } if (appPlatform.isDesktop) { KeyChangeEffect(chatModel.chatId.value) { if (chatModel.chatId.value != null) { @@ -302,7 +295,7 @@ expect fun DesktopActiveCallOverlayLayout(newChatSheetState: MutableStateFlow Unit)>() + private var alertViews = mutableStateListOf<(@Composable () -> Unit)>() fun showAlert(alert: @Composable () -> Unit) { Log.d(TAG, "AlertManager.showAlert") @@ -35,6 +35,12 @@ class AlertManager { alertViews.removeLastOrNull() } + fun hideAllAlerts() { + alertViews.clear() + } + + fun hasAlertsShown() = alertViews.isNotEmpty() + fun showAlertDialogButtons( title: String, text: String? = null, @@ -220,6 +226,7 @@ class AlertManager { companion object { val shared = AlertManager() + val privacySensitive = AlertManager() } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/localauth/LocalAuthView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/localauth/LocalAuthView.kt index c64c3dd29..468dd8580 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/localauth/LocalAuthView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/localauth/LocalAuthView.kt @@ -70,7 +70,8 @@ private fun deleteStorageAndRestart(m: ChatModel, password: String, completed: ( m.controller.startChat(createdUser) } ModalManager.fullscreen.closeModals() - AlertManager.shared.hideAlert() + AlertManager.shared.hideAllAlerts() + AlertManager.privacySensitive.hideAllAlerts() completed(LAResult.Success) } catch (e: Exception) { completed(LAResult.Error(generalGetString(MR.strings.incorrect_passcode))) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ScanToConnectView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ScanToConnectView.kt index 2439b16c3..9f28074ae 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ScanToConnectView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ScanToConnectView.kt @@ -20,7 +20,7 @@ import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.* import chat.simplex.common.views.chatlist.* import chat.simplex.common.views.helpers.* -import chat.simplex.common.views.usersettings.* +import chat.simplex.common.views.usersettings.IncognitoView import chat.simplex.res.MR import java.net.URI @@ -58,7 +58,7 @@ suspend fun planAndConnect( InvitationLinkPlan.OwnLink -> { Log.d(TAG, "planAndConnect, .InvitationLink, .OwnLink, incognito=$incognito") if (incognito != null) { - AlertManager.shared.showAlertDialog( + AlertManager.privacySensitive.showAlertDialog( title = generalGetString(MR.strings.connect_plan_connect_to_yourself), text = generalGetString(MR.strings.connect_plan_this_is_your_own_one_time_link), confirmText = if (incognito) generalGetString(MR.strings.connect_via_link_incognito) else generalGetString(MR.strings.connect_via_link_verb), @@ -80,13 +80,13 @@ suspend fun planAndConnect( val contact = connectionPlan.invitationLinkPlan.contact_ if (contact != null) { openKnownContact(chatModel, rhId, close, contact) - AlertManager.shared.showAlertMsg( + AlertManager.privacySensitive.showAlertMsg( generalGetString(MR.strings.contact_already_exists), String.format(generalGetString(MR.strings.connect_plan_you_are_already_connecting_to_vName), contact.displayName), hostDevice = hostDevice(rhId), ) } else { - AlertManager.shared.showAlertMsg( + AlertManager.privacySensitive.showAlertMsg( generalGetString(MR.strings.connect_plan_already_connecting), generalGetString(MR.strings.connect_plan_you_are_already_connecting_via_this_one_time_link), hostDevice = hostDevice(rhId), @@ -97,7 +97,7 @@ suspend fun planAndConnect( Log.d(TAG, "planAndConnect, .InvitationLink, .Known, incognito=$incognito") val contact = connectionPlan.invitationLinkPlan.contact openKnownContact(chatModel, rhId, close, contact) - AlertManager.shared.showAlertMsg( + AlertManager.privacySensitive.showAlertMsg( generalGetString(MR.strings.contact_already_exists), String.format(generalGetString(MR.strings.you_are_already_connected_to_vName_via_this_link), contact.displayName), hostDevice = hostDevice(rhId), @@ -121,7 +121,7 @@ suspend fun planAndConnect( ContactAddressPlan.OwnLink -> { Log.d(TAG, "planAndConnect, .ContactAddress, .OwnLink, incognito=$incognito") if (incognito != null) { - AlertManager.shared.showAlertDialog( + AlertManager.privacySensitive.showAlertDialog( title = generalGetString(MR.strings.connect_plan_connect_to_yourself), text = generalGetString(MR.strings.connect_plan_this_is_your_own_simplex_address), confirmText = if (incognito) generalGetString(MR.strings.connect_via_link_incognito) else generalGetString(MR.strings.connect_via_link_verb), @@ -141,7 +141,7 @@ suspend fun planAndConnect( ContactAddressPlan.ConnectingConfirmReconnect -> { Log.d(TAG, "planAndConnect, .ContactAddress, .ConnectingConfirmReconnect, incognito=$incognito") if (incognito != null) { - AlertManager.shared.showAlertDialog( + AlertManager.privacySensitive.showAlertDialog( title = generalGetString(MR.strings.connect_plan_repeat_connection_request), text = generalGetString(MR.strings.connect_plan_you_have_already_requested_connection_via_this_address), confirmText = if (incognito) generalGetString(MR.strings.connect_via_link_incognito) else generalGetString(MR.strings.connect_via_link_verb), @@ -162,7 +162,7 @@ suspend fun planAndConnect( Log.d(TAG, "planAndConnect, .ContactAddress, .ConnectingProhibit, incognito=$incognito") val contact = connectionPlan.contactAddressPlan.contact openKnownContact(chatModel, rhId, close, contact) - AlertManager.shared.showAlertMsg( + AlertManager.privacySensitive.showAlertMsg( generalGetString(MR.strings.contact_already_exists), String.format(generalGetString(MR.strings.connect_plan_you_are_already_connecting_to_vName), contact.displayName), hostDevice = hostDevice(rhId), @@ -172,7 +172,7 @@ suspend fun planAndConnect( Log.d(TAG, "planAndConnect, .ContactAddress, .Known, incognito=$incognito") val contact = connectionPlan.contactAddressPlan.contact openKnownContact(chatModel, rhId, close, contact) - AlertManager.shared.showAlertMsg( + AlertManager.privacySensitive.showAlertMsg( generalGetString(MR.strings.contact_already_exists), String.format(generalGetString(MR.strings.you_are_already_connected_to_vName_via_this_link), contact.displayName), hostDevice = hostDevice(rhId), @@ -193,7 +193,7 @@ suspend fun planAndConnect( GroupLinkPlan.Ok -> { Log.d(TAG, "planAndConnect, .GroupLink, .Ok, incognito=$incognito") if (incognito != null) { - AlertManager.shared.showAlertDialog( + AlertManager.privacySensitive.showAlertDialog( title = generalGetString(MR.strings.connect_via_group_link), text = generalGetString(MR.strings.you_will_join_group), confirmText = if (incognito) generalGetString(MR.strings.join_group_incognito_button) else generalGetString(MR.strings.join_group_button), @@ -217,7 +217,7 @@ suspend fun planAndConnect( GroupLinkPlan.ConnectingConfirmReconnect -> { Log.d(TAG, "planAndConnect, .GroupLink, .ConnectingConfirmReconnect, incognito=$incognito") if (incognito != null) { - AlertManager.shared.showAlertDialog( + AlertManager.privacySensitive.showAlertDialog( title = generalGetString(MR.strings.connect_plan_repeat_join_request), text = generalGetString(MR.strings.connect_plan_you_are_already_joining_the_group_via_this_link), confirmText = if (incognito) generalGetString(MR.strings.join_group_incognito_button) else generalGetString(MR.strings.join_group_button), @@ -238,12 +238,12 @@ suspend fun planAndConnect( Log.d(TAG, "planAndConnect, .GroupLink, .ConnectingProhibit, incognito=$incognito") val groupInfo = connectionPlan.groupLinkPlan.groupInfo_ if (groupInfo != null) { - AlertManager.shared.showAlertMsg( + AlertManager.privacySensitive.showAlertMsg( generalGetString(MR.strings.connect_plan_group_already_exists), String.format(generalGetString(MR.strings.connect_plan_you_are_already_joining_the_group_vName), groupInfo.displayName) ) } else { - AlertManager.shared.showAlertMsg( + AlertManager.privacySensitive.showAlertMsg( generalGetString(MR.strings.connect_plan_already_joining_the_group), generalGetString(MR.strings.connect_plan_you_are_already_joining_the_group_via_this_link), hostDevice = hostDevice(rhId), @@ -254,7 +254,7 @@ suspend fun planAndConnect( Log.d(TAG, "planAndConnect, .GroupLink, .Known, incognito=$incognito") val groupInfo = connectionPlan.groupLinkPlan.groupInfo openKnownGroup(chatModel, rhId, close, groupInfo) - AlertManager.shared.showAlertMsg( + AlertManager.privacySensitive.showAlertMsg( generalGetString(MR.strings.connect_plan_group_already_exists), String.format(generalGetString(MR.strings.connect_plan_you_are_already_in_group_vName), groupInfo.displayName), hostDevice = hostDevice(rhId), @@ -289,7 +289,7 @@ suspend fun connectViaUri( if (pcc != null) { chatModel.updateContactConnection(rhId, pcc) close?.invoke() - AlertManager.shared.showAlertMsg( + AlertManager.privacySensitive.showAlertMsg( title = generalGetString(MR.strings.connection_request_sent), text = when (connLinkType) { @@ -320,14 +320,14 @@ fun askCurrentOrIncognitoProfileAlert( text: AnnotatedString? = null, connectDestructive: Boolean, ) { - AlertManager.shared.showAlertDialogButtonsColumn( + AlertManager.privacySensitive.showAlertDialogButtonsColumn( title = title, text = text, buttons = { Column { val connectColor = if (connectDestructive) MaterialTheme.colors.error else MaterialTheme.colors.primary SectionItemView({ - AlertManager.shared.hideAlert() + AlertManager.privacySensitive.hideAlert() withApi { connectViaUri(chatModel, rhId, uri, incognito = false, connectionPlan, close) } @@ -335,7 +335,7 @@ fun askCurrentOrIncognitoProfileAlert( Text(generalGetString(MR.strings.connect_use_current_profile), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = connectColor) } SectionItemView({ - AlertManager.shared.hideAlert() + AlertManager.privacySensitive.hideAlert() withApi { connectViaUri(chatModel, rhId, uri, incognito = true, connectionPlan, close) } @@ -343,7 +343,7 @@ fun askCurrentOrIncognitoProfileAlert( Text(generalGetString(MR.strings.connect_use_new_incognito_profile), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = connectColor) } SectionItemView({ - AlertManager.shared.hideAlert() + AlertManager.privacySensitive.hideAlert() }) { Text(stringResource(MR.strings.cancel_verb), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) } @@ -372,14 +372,14 @@ fun ownGroupLinkConfirmConnect( groupInfo: GroupInfo, close: (() -> Unit)?, ) { - AlertManager.shared.showAlertDialogButtonsColumn( + AlertManager.privacySensitive.showAlertDialogButtonsColumn( title = generalGetString(MR.strings.connect_plan_join_your_group), text = AnnotatedString(String.format(generalGetString(MR.strings.connect_plan_this_is_your_link_for_group_vName), groupInfo.displayName)), buttons = { Column { // Open group SectionItemView({ - AlertManager.shared.hideAlert() + AlertManager.privacySensitive.hideAlert() openKnownGroup(chatModel, rhId, close, groupInfo) }) { Text(generalGetString(MR.strings.connect_plan_open_group), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) @@ -387,7 +387,7 @@ fun ownGroupLinkConfirmConnect( if (incognito != null) { // Join incognito / Join with current profile SectionItemView({ - AlertManager.shared.hideAlert() + AlertManager.privacySensitive.hideAlert() withApi { connectViaUri(chatModel, rhId, uri, incognito, connectionPlan, close) } @@ -400,7 +400,7 @@ fun ownGroupLinkConfirmConnect( } else { // Use current profile SectionItemView({ - AlertManager.shared.hideAlert() + AlertManager.privacySensitive.hideAlert() withApi { connectViaUri(chatModel, rhId, uri, incognito = false, connectionPlan, close) } @@ -409,7 +409,7 @@ fun ownGroupLinkConfirmConnect( } // Use new incognito profile SectionItemView({ - AlertManager.shared.hideAlert() + AlertManager.privacySensitive.hideAlert() withApi { connectViaUri(chatModel, rhId, uri, incognito = true, connectionPlan, close) } @@ -419,7 +419,7 @@ fun ownGroupLinkConfirmConnect( } // Cancel SectionItemView({ - AlertManager.shared.hideAlert() + AlertManager.privacySensitive.hideAlert() }) { Text(stringResource(MR.strings.cancel_verb), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) } From 6fa0001ea72e80fd1124f59efa1f53ad665398cf Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Mon, 18 Dec 2023 10:36:25 +0000 Subject: [PATCH 05/13] ios: delay suspendChat in NSE, background schedule depends on notifications mode (#3561) * ios: delay suspendChat in NSE * different background refresh interval depending on the settings * simplify * comment * reduce NSE suspend interval * space --- apps/ios/Shared/AppDelegate.swift | 2 +- apps/ios/Shared/Model/BGManager.swift | 30 +++++++++++++++---- .../ios/SimpleX NSE/NotificationService.swift | 25 ++++++++++++---- apps/ios/SimpleXChat/AppGroup.swift | 3 ++ 4 files changed, 49 insertions(+), 11 deletions(-) diff --git a/apps/ios/Shared/AppDelegate.swift b/apps/ios/Shared/AppDelegate.swift index bb1de9435..145e36279 100644 --- a/apps/ios/Shared/AppDelegate.swift +++ b/apps/ios/Shared/AppDelegate.swift @@ -81,7 +81,7 @@ class AppDelegate: NSObject, UIApplicationDelegate { } } else if let checkMessages = ntfData["checkMessages"] as? Bool, checkMessages { logger.debug("AppDelegate: didReceiveRemoteNotification: checkMessages") - if m.ntfEnablePeriodic && allowBackgroundRefresh() { + if m.ntfEnablePeriodic && allowBackgroundRefresh() && BGManager.shared.lastRanLongAgo { receiveMessages(completionHandler) } else { completionHandler(.noData) diff --git a/apps/ios/Shared/Model/BGManager.swift b/apps/ios/Shared/Model/BGManager.swift index a39155efe..25eab6c69 100644 --- a/apps/ios/Shared/Model/BGManager.swift +++ b/apps/ios/Shared/Model/BGManager.swift @@ -16,7 +16,12 @@ private let receiveTaskId = "chat.simplex.app.receive" private let waitForMessages: TimeInterval = 6 // This is the smallest interval between refreshes, and also target interval in "off" mode -private let bgRefreshInterval: TimeInterval = 600 +private let bgRefreshInterval: TimeInterval = 600 // 10 minutes + +// This intervals are used for background refresh in instant and periodic modes +private let periodicBgRefreshInterval: TimeInterval = 1200 // 20 minutes + +private let maxBgRefreshInterval: TimeInterval = 2400 // 40 minutes private let maxTimerCount = 9 @@ -34,14 +39,14 @@ class BGManager { } } - func schedule() { + func schedule(interval: TimeInterval? = nil) { if !ChatModel.shared.ntfEnableLocal { logger.debug("BGManager.schedule: disabled") return } logger.debug("BGManager.schedule") let request = BGAppRefreshTaskRequest(identifier: receiveTaskId) - request.earliestBeginDate = Date(timeIntervalSinceNow: bgRefreshInterval) + request.earliestBeginDate = Date(timeIntervalSinceNow: interval ?? runInterval) do { try BGTaskScheduler.shared.submit(request) } catch { @@ -49,20 +54,34 @@ class BGManager { } } + var runInterval: TimeInterval { + switch ChatModel.shared.notificationMode { + case .instant: maxBgRefreshInterval + case .periodic: periodicBgRefreshInterval + case .off: bgRefreshInterval + } + } + + var lastRanLongAgo: Bool { + Date.now.timeIntervalSince(chatLastBackgroundRunGroupDefault.get()) > runInterval + } + private func handleRefresh(_ task: BGAppRefreshTask) { if !ChatModel.shared.ntfEnableLocal { logger.debug("BGManager.handleRefresh: disabled") return } logger.debug("BGManager.handleRefresh") - schedule() - if allowBackgroundRefresh() { + let shouldRun_ = lastRanLongAgo + if allowBackgroundRefresh() && shouldRun_ { + schedule() let completeRefresh = completionHandler { task.setTaskCompleted(success: true) } task.expirationHandler = { completeRefresh("expirationHandler") } receiveMessages(completeRefresh) } else { + schedule(interval: shouldRun_ ? bgRefreshInterval : runInterval) logger.debug("BGManager.completionHandler: already active, not started") task.setTaskCompleted(success: true) } @@ -91,6 +110,7 @@ class BGManager { } self.completed = false DispatchQueue.main.async { + chatLastBackgroundRunGroupDefault.set(Date.now) let m = ChatModel.shared if (!m.chatInitialized) { setAppState(.bgRefresh) diff --git a/apps/ios/SimpleX NSE/NotificationService.swift b/apps/ios/SimpleX NSE/NotificationService.swift index eaa1131eb..c286ee1c3 100644 --- a/apps/ios/SimpleX NSE/NotificationService.swift +++ b/apps/ios/SimpleX NSE/NotificationService.swift @@ -14,9 +14,11 @@ import SimpleXChat let logger = Logger() -let suspendingDelay: UInt64 = 2_500_000_000 +let appSuspendingDelay: UInt64 = 2_500_000_000 -let nseSuspendTimeout: Int = 10 +let nseSuspendDelay: TimeInterval = 2 + +let nseSuspendTimeout: Int = 5 typealias NtfStream = ConcurrentQueue @@ -177,6 +179,10 @@ class NSEThreads { return false } } + + var noThreads: Bool { + allThreads.isEmpty + } } // Notification service extension creates a new instance of the class and calls didReceive for each notification. @@ -261,7 +267,7 @@ class NotificationService: UNNotificationServiceExtension { let dbStatus = startChat() if case .ok = dbStatus, let ntfInfo = apiGetNtfMessage(nonce: nonce, encNtfInfo: encNtfInfo) { - logger.debug("NotificationService: receiveNtfMessages: apiGetNtfMessage \(String(describing: ntfInfo), privacy: .public)") + logger.debug("NotificationService: receiveNtfMessages: apiGetNtfMessage \(String(describing: ntfInfo.ntfMessages.count), privacy: .public)") if let connEntity = ntfInfo.connEntity_ { setBestAttemptNtf( ntfInfo.ntfsEnabled @@ -326,7 +332,15 @@ class NotificationService: UNNotificationServiceExtension { if let t = threadId { threadId = nil if NSEThreads.shared.endThread(t) { - suspendChat(nseSuspendTimeout) + logger.debug("NotificationService.deliverBestAttemptNtf: will suspend") + // suspension is delayed to allow chat core finalise any processing + // (e.g., send delivery receipts) + DispatchQueue.global().asyncAfter(deadline: .now() + nseSuspendDelay) { + if NSEThreads.shared.noThreads { + logger.debug("NotificationService.deliverBestAttemptNtf: suspending...") + suspendChat(nseSuspendTimeout) + } + } } } if let handler = contentHandler, let ntf = bestAttemptNtf { @@ -497,7 +511,7 @@ func suspendChat(_ timeout: Int) { NSEChatState.shared.set(.suspending) if apiSuspendChat(timeoutMicroseconds: timeout * 1000000) { - logger.debug("NotificationService: activateChat: after apiActivateChat") + logger.debug("NotificationService: suspendChat: after apiSuspendChat") DispatchQueue.global().asyncAfter(deadline: .now() + Double(timeout) + 1, execute: chatSuspended) } else { NSEChatState.shared.set(state) @@ -510,6 +524,7 @@ func chatSuspended() { if case .suspending = NSEChatState.shared.value { NSEChatState.shared.set(.suspended) chatCloseStore() + logger.debug("NotificationService chatSuspended: suspended") } } diff --git a/apps/ios/SimpleXChat/AppGroup.swift b/apps/ios/SimpleXChat/AppGroup.swift index 10625e2ed..f79c294e0 100644 --- a/apps/ios/SimpleXChat/AppGroup.swift +++ b/apps/ios/SimpleXChat/AppGroup.swift @@ -15,6 +15,7 @@ let GROUP_DEFAULT_APP_STATE = "appState" let GROUP_DEFAULT_NSE_STATE = "nseState" let GROUP_DEFAULT_DB_CONTAINER = "dbContainer" public let GROUP_DEFAULT_CHAT_LAST_START = "chatLastStart" +public let GROUP_DEFAULT_CHAT_LAST_BACKGROUND_RUN = "chatLastBackgroundRun" let GROUP_DEFAULT_NTF_PREVIEW_MODE = "ntfPreviewMode" public let GROUP_DEFAULT_NTF_ENABLE_LOCAL = "ntfEnableLocal" // no longer used public let GROUP_DEFAULT_NTF_ENABLE_PERIODIC = "ntfEnablePeriodic" // no longer used @@ -156,6 +157,8 @@ public let dbContainerGroupDefault = EnumDefault( public let chatLastStartGroupDefault = DateDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_CHAT_LAST_START) +public let chatLastBackgroundRunGroupDefault = DateDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_CHAT_LAST_BACKGROUND_RUN) + public let ntfPreviewModeGroupDefault = EnumDefault( defaults: groupDefaults, forKey: GROUP_DEFAULT_NTF_PREVIEW_MODE, From f0338a03d1c986c33fb91711a58ea12e5ee21836 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Mon, 18 Dec 2023 10:41:08 +0000 Subject: [PATCH 06/13] directory: better search, allow both simplex:/ and simplex.chat links in description (#3546) * directory: new commands * better search * search test * return group links in simplex.chat domain, allow both simplex:/ and simplex.chat links in group description --- .../src/Directory/Events.hs | 24 +++- .../src/Directory/Options.hs | 2 + .../src/Directory/Search.hs | 32 +++++ .../src/Directory/Service.hs | 128 ++++++++++++++---- simplex-chat.cabal | 2 + tests/Bots/DirectoryTests.hs | 95 ++++++++++++- 6 files changed, 250 insertions(+), 33 deletions(-) create mode 100644 apps/simplex-directory-service/src/Directory/Search.hs diff --git a/apps/simplex-directory-service/src/Directory/Events.hs b/apps/simplex-directory-service/src/Directory/Events.hs index 89231e4db..a187ac3e8 100644 --- a/apps/simplex-directory-service/src/Directory/Events.hs +++ b/apps/simplex-directory-service/src/Directory/Events.hs @@ -21,14 +21,18 @@ where import Control.Applicative ((<|>)) import Data.Attoparsec.Text (Parser) import qualified Data.Attoparsec.Text as A +import Data.Functor (($>)) import Data.Text (Text) import qualified Data.Text as T +import Data.Text.Encoding (encodeUtf8) import Directory.Store import Simplex.Chat.Controller import Simplex.Chat.Messages import Simplex.Chat.Messages.CIContent import Simplex.Chat.Protocol (MsgContent (..)) import Simplex.Chat.Types +import Simplex.Messaging.Encoding.String +import Simplex.Messaging.Util ((<$?>)) import Data.Char (isSpace) import Data.Either (fromRight) @@ -83,6 +87,10 @@ deriving instance Show (SDirectoryRole r) data DirectoryCmdTag (r :: DirectoryRole) where DCHelp_ :: DirectoryCmdTag 'DRUser + DCSearchNext_ :: DirectoryCmdTag 'DRUser + DCAllGroups_ :: DirectoryCmdTag 'DRUser + DCRecentGroups_ :: DirectoryCmdTag 'DRUser + DCSubmitGroup_ :: DirectoryCmdTag 'DRUser DCConfirmDuplicateGroup_ :: DirectoryCmdTag 'DRUser DCListUserGroups_ :: DirectoryCmdTag 'DRUser DCDeleteGroup_ :: DirectoryCmdTag 'DRUser @@ -100,6 +108,10 @@ data ADirectoryCmdTag = forall r. ADCT (SDirectoryRole r) (DirectoryCmdTag r) data DirectoryCmd (r :: DirectoryRole) where DCHelp :: DirectoryCmd 'DRUser DCSearchGroup :: Text -> DirectoryCmd 'DRUser + DCSearchNext :: DirectoryCmd 'DRUser + DCAllGroups :: DirectoryCmd 'DRUser + DCRecentGroups :: DirectoryCmd 'DRUser + DCSubmitGroup :: ConnReqContact -> DirectoryCmd 'DRUser DCConfirmDuplicateGroup :: UserGroupRegId -> GroupName -> DirectoryCmd 'DRUser DCListUserGroups :: DirectoryCmd 'DRUser DCDeleteGroup :: UserGroupRegId -> GroupName -> DirectoryCmd 'DRUser @@ -120,7 +132,9 @@ deriving instance Show ADirectoryCmd directoryCmdP :: Parser ADirectoryCmd directoryCmdP = - (A.char '/' *> cmdStrP) <|> (ADC SDRUser . DCSearchGroup <$> A.takeText) + (A.char '/' *> cmdStrP) + <|> (A.char '.' $> ADC SDRUser DCSearchNext) + <|> (ADC SDRUser . DCSearchGroup <$> A.takeText) where cmdStrP = (tagP >>= \(ADCT u t) -> ADC u <$> (cmdP t <|> pure (DCCommandError t))) @@ -128,6 +142,10 @@ directoryCmdP = tagP = A.takeTill (== ' ') >>= \case "help" -> u DCHelp_ "h" -> u DCHelp_ + "next" -> u DCSearchNext_ + "all" -> u DCAllGroups_ + "new" -> u DCRecentGroups_ + "submit" -> u DCSubmitGroup_ "confirm" -> u DCConfirmDuplicateGroup_ "list" -> u DCListUserGroups_ "ls" -> u DCListUserGroups_ @@ -146,6 +164,10 @@ directoryCmdP = cmdP :: DirectoryCmdTag r -> Parser (DirectoryCmd r) cmdP = \case DCHelp_ -> pure DCHelp + DCSearchNext_ -> pure DCSearchNext + DCAllGroups_ -> pure DCAllGroups + DCRecentGroups_ -> pure DCRecentGroups + DCSubmitGroup_ -> fmap DCSubmitGroup . strDecode . encodeUtf8 <$?> (A.takeWhile1 isSpace *> A.takeText) DCConfirmDuplicateGroup_ -> gc DCConfirmDuplicateGroup DCListUserGroups_ -> pure DCListUserGroups DCDeleteGroup_ -> gc DCDeleteGroup diff --git a/apps/simplex-directory-service/src/Directory/Options.hs b/apps/simplex-directory-service/src/Directory/Options.hs index 0ca8cee78..6d4e1296f 100644 --- a/apps/simplex-directory-service/src/Directory/Options.hs +++ b/apps/simplex-directory-service/src/Directory/Options.hs @@ -21,6 +21,7 @@ data DirectoryOpts = DirectoryOpts superUsers :: [KnownContact], directoryLog :: Maybe FilePath, serviceName :: String, + searchResults :: Int, testing :: Bool } @@ -54,6 +55,7 @@ directoryOpts appDir defaultDbFileName = do superUsers, directoryLog, serviceName, + searchResults = 10, testing = False } diff --git a/apps/simplex-directory-service/src/Directory/Search.hs b/apps/simplex-directory-service/src/Directory/Search.hs new file mode 100644 index 000000000..822182b05 --- /dev/null +++ b/apps/simplex-directory-service/src/Directory/Search.hs @@ -0,0 +1,32 @@ +{-# LANGUAGE DuplicateRecordFields #-} +{-# LANGUAGE NamedFieldPuns #-} + +module Directory.Search where + +import Data.List (sortOn) +import Data.Ord (Down (..)) +import Data.Set (Set) +import qualified Data.Set as S +import Data.Text (Text) +import Data.Time.Clock (UTCTime) +import Simplex.Chat.Types + +data SearchRequest = SearchRequest + { searchType :: SearchType, + searchTime :: UTCTime, + sentGroups :: Set GroupId + } + +data SearchType = STAll | STRecent | STSearch Text + +takeTop :: Int -> [(GroupInfo, GroupSummary)] -> [(GroupInfo, GroupSummary)] +takeTop n = take n . sortOn (Down . currentMembers . snd) + +takeRecent :: Int -> [(GroupInfo, GroupSummary)] -> [(GroupInfo, GroupSummary)] +takeRecent n = take n . sortOn (Down . (\GroupInfo {createdAt} -> createdAt) . fst) + +groupIds :: [(GroupInfo, GroupSummary)] -> Set GroupId +groupIds = S.fromList . map (\(GroupInfo {groupId}, _) -> groupId) + +filterNotSent :: Set GroupId -> [(GroupInfo, GroupSummary)] -> [(GroupInfo, GroupSummary)] +filterNotSent sentGroups = filter (\(GroupInfo {groupId}, _) -> groupId `S.notMember` sentGroups) diff --git a/apps/simplex-directory-service/src/Directory/Service.hs b/apps/simplex-directory-service/src/Directory/Service.hs index fb187bbeb..ea79dabb1 100644 --- a/apps/simplex-directory-service/src/Directory/Service.hs +++ b/apps/simplex-directory-service/src/Directory/Service.hs @@ -17,16 +17,16 @@ import Control.Concurrent.Async import Control.Concurrent.STM import Control.Monad import qualified Data.ByteString.Char8 as B -import Data.List (sortOn) import Data.Maybe (fromMaybe, maybeToList) -import Data.Ord (Down(..)) +import Data.Set (Set) import qualified Data.Set as S import Data.Text (Text) import qualified Data.Text as T -import Data.Time.Clock (getCurrentTime) +import Data.Time.Clock (diffUTCTime, getCurrentTime) import Data.Time.LocalTime (getCurrentTimeZone) import Directory.Events import Directory.Options +import Directory.Search import Directory.Store import Simplex.Chat.Bot import Simplex.Chat.Bot.KnownContacts @@ -36,8 +36,10 @@ import Simplex.Chat.Messages import Simplex.Chat.Options import Simplex.Chat.Protocol (MsgContent (..)) import Simplex.Chat.Types -import Simplex.Chat.View (serializeChatResponse) +import Simplex.Chat.View (serializeChatResponse, simplexChatContact) import Simplex.Messaging.Encoding.String +import Simplex.Messaging.TMap (TMap) +import qualified Simplex.Messaging.TMap as TM import Simplex.Messaging.Util (safeDecodeUtf8, tshow, ($>>=), (<$$>)) import System.Directory (getAppUserDataDirectory) @@ -55,6 +57,15 @@ data GroupRolesStatus | GRSBadRoles deriving (Eq) +data ServiceState = ServiceState + { searchRequests :: TMap ContactId SearchRequest + } + +newServiceState :: IO ServiceState +newServiceState = do + searchRequests <- atomically TM.empty + pure ServiceState {searchRequests} + welcomeGetOpts :: IO DirectoryOpts welcomeGetOpts = do appDir <- getAppUserDataDirectory "simplex" @@ -65,8 +76,9 @@ welcomeGetOpts = do pure opts directoryService :: DirectoryStore -> DirectoryOpts -> User -> ChatController -> IO () -directoryService st DirectoryOpts {superUsers, serviceName, testing} user@User {userId} cc = do +directoryService st DirectoryOpts {superUsers, serviceName, searchResults, testing} user@User {userId} cc = do initializeBotAddress' (not testing) cc + env <- newServiceState race_ (forever $ void getLine) . forever $ do (_, _, resp) <- atomically . readTBQueue $ outputQ cc forM_ (crDirectoryEvent resp) $ \case @@ -84,7 +96,7 @@ directoryService st DirectoryOpts {superUsers, serviceName, testing} user@User { DEItemEditIgnored _ct -> pure () DEItemDeleteIgnored _ct -> pure () DEContactCommand ct ciId aCmd -> case aCmd of - ADC SDRUser cmd -> deUserCommand ct ciId cmd + ADC SDRUser cmd -> deUserCommand env ct ciId cmd ADC SDRSuperUser cmd -> deSuperUserCommand ct ciId cmd where withSuperUsers action = void . forkIO $ forM_ superUsers $ \KnownContact {contactId} -> action contactId @@ -105,8 +117,11 @@ directoryService st DirectoryOpts {superUsers, serviceName, testing} user@User { T.unpack $ "The group " <> displayName <> " (" <> fullName <> ") is already listed in the directory, please choose another name." getGroups :: Text -> IO (Maybe [(GroupInfo, GroupSummary)]) - getGroups search = - sendChatCmd cc (APIListGroups userId Nothing $ Just $ T.unpack search) >>= \case + getGroups = getGroups_ . Just + + getGroups_ :: Maybe Text -> IO (Maybe [(GroupInfo, GroupSummary)]) + getGroups_ search_ = + sendChatCmd cc (APIListGroups userId Nothing $ T.unpack <$> search_) >>= \case CRGroupsList {groups} -> pure $ Just groups _ -> pure Nothing @@ -140,7 +155,8 @@ directoryService st DirectoryOpts {superUsers, serviceName, testing} user@User { sendMessage cc ct $ "Welcome to " <> serviceName <> " service!\n\ \Send a search string to find groups or */help* to learn how to add groups to directory.\n\n\ - \For example, send _privacy_ to find groups about privacy.\n\n\ + \For example, send _privacy_ to find groups about privacy.\n\ + \Or send */all* or */new* to list groups.\n\n\ \Content and privacy policy: https://simplex.chat/docs/directory.html" deGroupInvitation :: Contact -> GroupInfo -> GroupMemberRole -> GroupMemberRole -> IO () @@ -201,7 +217,7 @@ directoryService st DirectoryOpts {superUsers, serviceName, testing} user@User { "Created the public link to join the group via this directory service that is always online.\n\n\ \Please add it to the group welcome message.\n\ \For example, add:" - notifyOwner gr $ "Link to join the group " <> T.unpack displayName <> ": " <> B.unpack (strEncode connReqContact) + notifyOwner gr $ "Link to join the group " <> T.unpack displayName <> ": " <> B.unpack (strEncode $ simplexChatContact connReqContact) CRChatCmdError _ (ChatError e) -> case e of CEGroupUserRole {} -> notifyOwner gr "Failed creating group link, as service is no longer an admin." CEGroupMemberUserRemoved -> notifyOwner gr "Failed creating group link, as service is removed from the group." @@ -276,9 +292,10 @@ directoryService st DirectoryOpts {superUsers, serviceName, testing} user@User { where profileUpdate = \case CRGroupLink {connReqContact} -> - let groupLink = safeDecodeUtf8 $ strEncode connReqContact - hadLinkBefore = groupLink `isInfix` description p - hasLinkNow = groupLink `isInfix` description p' + let groupLink1 = safeDecodeUtf8 $ strEncode connReqContact + groupLink2 = safeDecodeUtf8 $ strEncode $ simplexChatContact connReqContact + hadLinkBefore = groupLink1 `isInfix` description p || groupLink2 `isInfix` description p + hasLinkNow = groupLink1 `isInfix` description p' || groupLink2 `isInfix` description p' in if | hadLinkBefore && hasLinkNow -> GPHasServiceLink | hadLinkBefore -> GPServiceLinkRemoved @@ -379,8 +396,8 @@ directoryService st DirectoryOpts {superUsers, serviceName, testing} user@User { notifyOwner gr $ serviceName <> " is removed from the group " <> userGroupReference gr g <> ".\n\nThe group is no longer listed in the directory." notifySuperUsers $ "The group " <> groupReference g <> " is de-listed (directory service is removed)." - deUserCommand :: Contact -> ChatItemId -> DirectoryCmd 'DRUser -> IO () - deUserCommand ct ciId = \case + deUserCommand :: ServiceState -> Contact -> ChatItemId -> DirectoryCmd 'DRUser -> IO () + deUserCommand env@ServiceState {searchRequests} ct ciId = \case DCHelp -> sendMessage cc ct $ "You must be the owner to add the group to the directory:\n\ @@ -389,20 +406,25 @@ directoryService st DirectoryOpts {superUsers, serviceName, testing} user@User { \3. You will then need to add this link to the group welcome message.\n\ \4. Once the link is added, service admins will approve the group (it can take up to 24 hours), and everybody will be able to find it in directory.\n\n\ \Start from inviting the bot to your group as admin - it will guide you through the process" - DCSearchGroup s -> - getGroups s >>= \case - Just groups -> - atomically (filterListedGroups st groups) >>= \case - [] -> sendReply "No groups found" - gs -> do - sendReply $ "Found " <> show (length gs) <> " group(s)" <> if length gs > 10 then ", sending 10." else "" - void . forkIO $ forM_ (take 10 $ sortOn (Down . currentMembers . snd) gs) $ - \(GroupInfo {groupProfile = p@GroupProfile {image = image_}}, GroupSummary {currentMembers}) -> do - let membersStr = "_" <> tshow currentMembers <> " members_" - text = groupInfoText p <> "\n" <> membersStr - msg = maybe (MCText text) (\image -> MCImage {text, image}) image_ - sendComposedMessage cc ct Nothing msg - Nothing -> sendReply "Error: getGroups. Please notify the developers." + DCSearchGroup s -> withFoundListedGroups (Just s) $ sendSearchResults s + DCSearchNext -> + atomically (TM.lookup (contactId' ct) searchRequests) >>= \case + Just search@SearchRequest {searchType, searchTime} -> do + currentTime <- getCurrentTime + if diffUTCTime currentTime searchTime > 300 -- 5 minutes + then do + atomically $ TM.delete (contactId' ct) searchRequests + showAllGroups + else case searchType of + STSearch s -> withFoundListedGroups (Just s) $ sendNextSearchResults takeTop search + STAll -> withFoundListedGroups Nothing $ sendNextSearchResults takeTop search + STRecent -> withFoundListedGroups Nothing $ sendNextSearchResults takeRecent search + Nothing -> showAllGroups + where + showAllGroups = deUserCommand env ct ciId DCAllGroups + DCAllGroups -> withFoundListedGroups Nothing $ sendAllGroups takeTop "top" STAll + DCRecentGroups -> withFoundListedGroups Nothing $ sendAllGroups takeRecent "the most recent" STRecent + DCSubmitGroup _link -> pure () DCConfirmDuplicateGroup ugrId gName -> atomically (getUserGroupReg st (contactId' ct) ugrId) >>= \case Nothing -> sendReply $ "Group ID " <> show ugrId <> " not found" @@ -429,6 +451,54 @@ directoryService st DirectoryOpts {superUsers, serviceName, testing} user@User { DCCommandError tag -> sendReply $ "Command error: " <> show tag where sendReply = sendComposedMessage cc ct (Just ciId) . textMsgContent + withFoundListedGroups s_ action = + getGroups_ s_ >>= \case + Just groups -> atomically (filterListedGroups st groups) >>= action + Nothing -> sendReply "Error: getGroups. Please notify the developers." + sendSearchResults s = \case + [] -> sendReply "No groups found" + gs -> do + let gs' = takeTop searchResults gs + moreGroups = length gs - length gs' + more = if moreGroups > 0 then ", sending top " <> show (length gs') else "" + sendReply $ "Found " <> show (length gs) <> " group(s)" <> more <> "." + updateSearchRequest (STSearch s) $ groupIds gs' + sendFoundGroups gs' moreGroups + sendAllGroups takeFirst sortName searchType = \case + [] -> sendReply "No groups listed" + gs -> do + let gs' = takeFirst searchResults gs + moreGroups = length gs - length gs' + more = if moreGroups > 0 then ", sending " <> sortName <> " " <> show (length gs') else "" + sendReply $ show (length gs) <> " group(s) listed" <> more <> "." + updateSearchRequest searchType $ groupIds gs' + sendFoundGroups gs' moreGroups + sendNextSearchResults takeFirst SearchRequest {searchType, sentGroups} = \case + [] -> do + sendReply "Sorry, no more groups" + atomically $ TM.delete (contactId' ct) searchRequests + gs -> do + let gs' = takeFirst searchResults $ filterNotSent sentGroups gs + sentGroups' = sentGroups <> groupIds gs' + moreGroups = length gs - S.size sentGroups' + sendReply $ "Sending " <> show (length gs') <> " more group(s)." + updateSearchRequest searchType sentGroups' + sendFoundGroups gs' moreGroups + updateSearchRequest :: SearchType -> Set GroupId -> IO () + updateSearchRequest searchType sentGroups = do + searchTime <- getCurrentTime + let search = SearchRequest {searchType, searchTime, sentGroups} + atomically $ TM.insert (contactId' ct) search searchRequests + sendFoundGroups gs moreGroups = + void . forkIO $ do + forM_ gs $ + \(GroupInfo {groupProfile = p@GroupProfile {image = image_}}, GroupSummary {currentMembers}) -> do + let membersStr = "_" <> tshow currentMembers <> " members_" + text = groupInfoText p <> "\n" <> membersStr + msg = maybe (MCText text) (\image -> MCImage {text, image}) image_ + sendComposedMessage cc ct Nothing msg + when (moreGroups > 0) $ + sendComposedMessage cc ct Nothing $ MCText $ "Send */next* or just *.* for " <> tshow moreGroups <> " more result(s)." deSuperUserCommand :: Contact -> ChatItemId -> DirectoryCmd 'DRSuperUser -> IO () deSuperUserCommand ct ciId cmd diff --git a/simplex-chat.cabal b/simplex-chat.cabal index ce066bc3c..f3918dfec 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -467,6 +467,7 @@ executable simplex-directory-service other-modules: Directory.Events Directory.Options + Directory.Search Directory.Service Directory.Store Paths_simplex_chat @@ -553,6 +554,7 @@ test-suite simplex-chat-test Broadcast.Options Directory.Events Directory.Options + Directory.Search Directory.Service Directory.Store Paths_simplex_chat diff --git a/tests/Bots/DirectoryTests.hs b/tests/Bots/DirectoryTests.hs index b31d6f36f..3c6991bb5 100644 --- a/tests/Bots/DirectoryTests.hs +++ b/tests/Bots/DirectoryTests.hs @@ -30,6 +30,7 @@ directoryServiceTests = do it "should suspend and resume group" testSuspendResume it "should join found group via link" testJoinGroup it "should support group names with spaces" testGroupNameWithSpaces + it "should return more groups in search, all and recent groups" testSearchGroups describe "de-listing the group" $ do it "should de-list if owner leaves the group" testDelistedOwnerLeaves it "should de-list if owner is removed from the group" testDelistedOwnerRemoved @@ -67,6 +68,7 @@ mkDirectoryOpts tmp superUsers = superUsers, directoryLog = Just $ tmp "directory_service.log", serviceName = "SimpleX-Directory", + searchResults = 3, testing = True } @@ -158,7 +160,7 @@ testDirectoryService tmp = search u s welcome = do u #> ("@SimpleX-Directory " <> s) u <# ("SimpleX-Directory> > " <> s) - u <## " Found 1 group(s)" + u <## " Found 1 group(s)." u <# "SimpleX-Directory> PSA (Privacy, Security & Anonymity)" u <## "Welcome message:" u <## welcome @@ -206,7 +208,7 @@ testJoinGroup tmp = cath `connectVia` dsLink cath #> "@SimpleX-Directory privacy" cath <# "SimpleX-Directory> > privacy" - cath <## " Found 1 group(s)" + cath <## " Found 1 group(s)." cath <# "SimpleX-Directory> privacy (Privacy)" cath <## "Welcome message:" welcomeMsg <- getTermLine cath @@ -263,6 +265,92 @@ testGroupNameWithSpaces tmp = bob <# "SimpleX-Directory> The group ID 1 (Privacy & Security) is listed in the directory again!" groupFound bob "Privacy & Security" +testSearchGroups :: HasCallStack => FilePath -> IO () +testSearchGroups tmp = + withDirectoryService tmp $ \superUser dsLink -> + withNewTestChat tmp "bob" bobProfile $ \bob -> do + withNewTestChat tmp "cath" cathProfile $ \cath -> do + bob `connectVia` dsLink + cath `connectVia` dsLink + forM_ [1..8 :: Int] $ \i -> registerGroupId superUser bob (groups !! (i - 1)) "" i i + connectUsers bob cath + fullAddMember "MyGroup" "" bob cath GRMember + joinGroup "MyGroup" cath bob + cath <## "#MyGroup: member SimpleX-Directory_1 is connected" + cath <## "contact and member are merged: SimpleX-Directory, #MyGroup SimpleX-Directory_1" + cath <## "use @SimpleX-Directory to send messages" + cath #> "@SimpleX-Directory MyGroup" + cath <# "SimpleX-Directory> > MyGroup" + cath <## " Found 7 group(s), sending top 3." + receivedGroup cath 0 3 + receivedGroup cath 1 2 + receivedGroup cath 2 2 + cath <# "SimpleX-Directory> Send /next or just . for 4 more result(s)." + cath #> "@SimpleX-Directory /next" + cath <# "SimpleX-Directory> > /next" + cath <## " Sending 3 more group(s)." + receivedGroup cath 3 2 + receivedGroup cath 4 2 + receivedGroup cath 5 2 + cath <# "SimpleX-Directory> Send /next or just . for 1 more result(s)." + -- search of another user does not affect the search of the first user + groupFound bob "Another" + cath #> "@SimpleX-Directory ." + cath <# "SimpleX-Directory> > ." + cath <## " Sending 1 more group(s)." + receivedGroup cath 6 2 + cath #> "@SimpleX-Directory /all" + cath <# "SimpleX-Directory> > /all" + cath <## " 8 group(s) listed, sending top 3." + receivedGroup cath 0 3 + receivedGroup cath 1 2 + receivedGroup cath 2 2 + cath <# "SimpleX-Directory> Send /next or just . for 5 more result(s)." + cath #> "@SimpleX-Directory /new" + cath <# "SimpleX-Directory> > /new" + cath <## " 8 group(s) listed, sending the most recent 3." + receivedGroup cath 7 2 + receivedGroup cath 6 2 + receivedGroup cath 5 2 + cath <# "SimpleX-Directory> Send /next or just . for 5 more result(s)." + cath #> "@SimpleX-Directory term3" + cath <# "SimpleX-Directory> > term3" + cath <## " Found 3 group(s)." + receivedGroup cath 4 2 + receivedGroup cath 5 2 + receivedGroup cath 6 2 + cath #> "@SimpleX-Directory term1" + cath <# "SimpleX-Directory> > term1" + cath <## " Found 6 group(s), sending top 3." + receivedGroup cath 1 2 + receivedGroup cath 2 2 + receivedGroup cath 3 2 + cath <# "SimpleX-Directory> Send /next or just . for 3 more result(s)." + cath #> "@SimpleX-Directory ." + cath <# "SimpleX-Directory> > ." + cath <## " Sending 3 more group(s)." + receivedGroup cath 4 2 + receivedGroup cath 5 2 + receivedGroup cath 6 2 + where + groups :: [String] + groups = + [ "MyGroup", + "MyGroup term1 1", + "MyGroup term1 2", + "MyGroup term1 term2", + "MyGroup term1 term2 term3", + "MyGroup term1 term2 term3 term4", + "MyGroup term1 term2 term3 term4 term5", + "Another" + ] + receivedGroup :: TestCC -> Int -> Int -> IO () + receivedGroup u ix count = do + u <#. ("SimpleX-Directory> " <> groups !! ix) + u <## "Welcome message:" + u <##. "Link to join the group " + u <## (show count <> " members") + testDelistedOwnerLeaves :: HasCallStack => FilePath -> IO () testDelistedOwnerLeaves tmp = withDirectoryServiceCfg tmp testCfgCreateGroupDirect $ \superUser dsLink -> @@ -930,6 +1018,7 @@ u `connectVia` dsLink = do u <## "Send a search string to find groups or /help to learn how to add groups to directory." u <## "" u <## "For example, send privacy to find groups about privacy." + u <## "Or send /all or /new to list groups." u <## "" u <## "Content and privacy policy: https://simplex.chat/docs/directory.html" @@ -967,7 +1056,7 @@ groupFoundN :: Int -> TestCC -> String -> IO () groupFoundN count u name = do u #> ("@SimpleX-Directory " <> name) u <# ("SimpleX-Directory> > " <> name) - u <## " Found 1 group(s)" + u <## " Found 1 group(s)." u <#. ("SimpleX-Directory> " <> name) u <## "Welcome message:" u <##. "Link to join the group " From ce9218b186f1d53ee8dff66c5b3cef53309ef76c Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Mon, 18 Dec 2023 22:04:49 +0400 Subject: [PATCH 07/13] ios: rework authentication (#3556) --- apps/ios/Shared/ContentView.swift | 157 +++++++++++------- apps/ios/Shared/Model/ChatModel.swift | 2 + apps/ios/Shared/SimpleXApp.swift | 52 ++---- .../Views/UserSettings/PrivacySettings.swift | 2 + 4 files changed, 118 insertions(+), 95 deletions(-) diff --git a/apps/ios/Shared/ContentView.swift b/apps/ios/Shared/ContentView.swift index b69ccbb7c..d7b9fef21 100644 --- a/apps/ios/Shared/ContentView.swift +++ b/apps/ios/Shared/ContentView.swift @@ -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: diff --git a/apps/ios/Shared/Model/ChatModel.swift b/apps/ios/Shared/Model/ChatModel.swift index e7932f2d9..0cc281fda 100644 --- a/apps/ios/Shared/Model/ChatModel.swift +++ b/apps/ios/Shared/Model/ChatModel.swift @@ -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] = [] diff --git a/apps/ios/Shared/SimpleXApp.swift b/apps/ios/Shared/SimpleXApp.swift index 057188c37..c023f375d 100644 --- a/apps/ios/Shared/SimpleXApp.swift +++ b/apps/ios/Shared/SimpleXApp.swift @@ -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() diff --git a/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift b/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift index 90b83fa4f..d8ff2c2f8 100644 --- a/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift +++ b/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift @@ -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: From 26a189917bb7a93b704dda09be2298d7c6e593e3 Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Tue, 19 Dec 2023 05:37:10 +0800 Subject: [PATCH 08/13] sctipt: check string formatting (#3570) * sctipt: check string formatting * all --- apps/multiplatform/common/build.gradle.kts | 64 +++++++++++++++++-- .../commonMain/resources/MR/ar/strings.xml | 1 - .../commonMain/resources/MR/el/strings.xml | 2 +- .../commonMain/resources/MR/es/strings.xml | 2 +- .../commonMain/resources/MR/fi/strings.xml | 1 - .../commonMain/resources/MR/ja/strings.xml | 2 - .../commonMain/resources/MR/tr/strings.xml | 6 +- .../resources/MR/zh-rCN/strings.xml | 3 +- .../resources/MR/zh-rTW/strings.xml | 2 +- 9 files changed, 67 insertions(+), 16 deletions(-) diff --git a/apps/multiplatform/common/build.gradle.kts b/apps/multiplatform/common/build.gradle.kts index 4b0e38d8a..32bfadd37 100644 --- a/apps/multiplatform/common/build.gradle.kts +++ b/apps/multiplatform/common/build.gradle.kts @@ -155,6 +155,34 @@ afterEvaluate { val endTagRegex = Regex("]*>.*(<|>).*|[^>]*>.*(<|>).*") val correctHtmlRegex = Regex("[^>]*>.*.*.*|[^>]*>.*.*.*|[^>]*>.*.*.*|[^>]*>.*]*>.*.*") + val possibleFormat = listOf("s", "d", "1\$s", "1\$d", "2s", "f") + + fun String.id(): String = replace(" { + if (!contains("%")) return emptyList() + val value = substringAfter("\">").substringBeforeLast("") + + val formats = ArrayList() + 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(" + 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>() + treeList.forEachIndexed { index, file -> + val isBase = index == 0 val initialLines = ArrayList() val finalLines = ArrayList() + val errors = ArrayList() + file.useLines { lines -> val multiline = ArrayList() 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 -> diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/ar/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/ar/strings.xml index 098c74835..fd5a827ba 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/ar/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/ar/strings.xml @@ -150,7 +150,6 @@ إضافة جهة اتصال جديدة : لإنشاء رمز الاستجابة السريعة الخاص بك لمرة واحدة لجهة اتصالك.]]> امسح رمز الاستجابة السريعة : للاتصال بجهة الاتصال التي تعرض لك رمز الاستجابة السريعة.]]> مكالمتك تحت الإجراء - انتهت المكالمة تغيير عبارة مرور قاعدة البيانات؟ لا يمكن الوصول إلى Keystore لحفظ كلمة مرور قاعدة البيانات إلغاء معاينة الملف diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/el/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/el/strings.xml index 714f31732..7063eb900 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/el/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/el/strings.xml @@ -41,7 +41,7 @@ Αποδοχή Αποδοχή αιτήματος σύνδεσης; αποδεκτή κλήση - Πρόσβαση στους διακομιστές μέσω SOCKS proxy στην πόρτα 9050; Ο διακομιστής μεσολάβησης (proxy server) πρέπει να είναι ενεργός πριν ενεργοποιηθεί αυτή η ρύθμιση. + Πρόσβαση στους διακομιστές μέσω SOCKS proxy στην πόρτα %d; Ο διακομιστής μεσολάβησης (proxy server) πρέπει να είναι ενεργός πριν ενεργοποιηθεί αυτή η ρύθμιση. Προσθήκη διακομιστή… Προχωρημένες ρυθμίσεις δικτύου Προσθήκη διακομιστών μέσω σάρωσης QR κωδικών. diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/es/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/es/strings.xml index 08cc7f982..381d28afa 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/es/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/es/strings.xml @@ -298,7 +298,7 @@ Cancelar mensaje en directo Confirmar Vaciar - Build de la aplicación + Build de la aplicación: %s ¡La llamada ha terminado! el servidor de envío ha cambiado para tí cancelar vista previa del enlace diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/fi/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/fi/strings.xml index ce8692130..5410778c4 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/fi/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/fi/strings.xml @@ -171,7 +171,6 @@ Arkisto Poista keskusteluarkisto\? Luotu %1$s - %s:n rooli muutettu %s:ksi poistettu ryhmä yhdistää yhdistäminen (hyväksytty) diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/ja/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/ja/strings.xml index 94a35dbd8..fb03dd1f2 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/ja/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/ja/strings.xml @@ -687,7 +687,6 @@ ファイル送信が中止されました。 送信元が繋がりリクエストを削除したかもしれません。 このサーバで待ち行列を作るには認証が必要です。パスワードをご確認ください。 - アプリが定期的に新しいメッセージを受信します。一日の電池使用量が約3%で、プッシュ通知に頼らずに、あなたの端末のデータをサーバに送ることはありません。 SimpleXロック 通知を受けるには、データベースの暗証フレーズを入力してください。 SimpleX Chat サービス @@ -904,7 +903,6 @@ SIMPLEX CHATを支援 テストサーバ 受信アドレスは別のサーバーに変更されます。アドレス変更は送信者がオンラインになった後に完了します。 - SimpleX バックグラウンド・サービス を使ってます。一日の電池使用量は約3%です。]]> あなたのプライバシーを守るために、他のアプリと違って、ユーザーIDの変わりに SimpleX メッセージ束毎にIDを配布し、各連絡先が別々と扱います。 あなたのチャットプロフィールが他のグループメンバーに送られます。 エンドツーエンド暗号化を確認するには、ご自分の端末と連絡先の端末のコードを比べます (スキャンします)。 diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/tr/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/tr/strings.xml index 9824b0d8f..d7df9655d 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/tr/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/tr/strings.xml @@ -337,7 +337,7 @@ Mesaj gönderilirken hata oluştu Adres oluştururken hata oluştu Adres değiştirirken hata oluştu - 1$s sizinle şu yolla bağlantı kurmak istiyor + %1$s sizinle şu yolla bağlantı kurmak istiyor Ayarları değiştirirken hata oluştu Toplu konuşma bağlantısı oluştururken hata oluştu Yetki değiştirirken hata oluştu @@ -747,9 +747,9 @@ Aklınızda bulunsun: kaybederseniz, parolayı kurtaramaz veya değiştiremezsiniz.]]> Sohbet arşivi SOHBET ARŞİVİ - 1$s grubuna davet + %1$s grubuna davet Gruba katıl\? - 1$s davet edildi + %1$s davet edildi grup bağlantınız üzerinden davet edildi davet edildi Gruba davet edin diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/zh-rCN/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/zh-rCN/strings.xml index ed2b9986c..31be2f187 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/zh-rCN/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/zh-rCN/strings.xml @@ -1344,7 +1344,7 @@ 我们错过的第二个\"√\"!✅ 设定数据库密码 为群组禁用回执吗? - %s、%s 和 %d 已连接 + %s、%s 和 %s 已连接 修复群组成员不支持的问题 已为 %d 组启用送达回执功能 重新协商 @@ -1427,7 +1427,6 @@ 通过链接进行连接吗? 已经加入了该群组! %s、 %s 和 %d 名成员 - %s 审核了 %d 条消息 解封成员 连接到你自己? 轻按连接 diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/zh-rTW/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/zh-rTW/strings.xml index b1d988e46..9caf45dcc 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/zh-rTW/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/zh-rTW/strings.xml @@ -11,7 +11,7 @@ 關於 SimpleX Chat 接受連接請求? 已接受通話 - 要在端口啟用 SOCKS 代理伺服器嗎?在啟用這個選項之前,必須先啟用代理伺服器。 + 要在端口啟用 SOCKS 代理伺服器嗎 %d?在啟用這個選項之前,必須先啟用代理伺服器。 管理員 然後,選按: 新增預設伺服器 From 5e042d222eeb54b570946499f2381d142d9f3672 Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Tue, 19 Dec 2023 18:24:13 +0800 Subject: [PATCH 09/13] desktop: saving qr code as an image (#3572) --- .../chat/simplex/common/views/newchat/QRCode.kt | 2 +- .../simplex/common/views/helpers/Utils.desktop.kt | 11 +++++------ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/QRCode.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/QRCode.kt index 763addae6..7f9fae60a 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/QRCode.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/QRCode.kt @@ -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)) } diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/Utils.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/Utils.desktop.kt index eb1792474..19c9fc0fd 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/Utils.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/Utils.desktop.kt @@ -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}") From 7b073ba9f83e808bc19cff24bbe2732dfd630bd9 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Tue, 19 Dec 2023 10:26:01 +0000 Subject: [PATCH 10/13] core: allow deleting last user (#3567) * core: allow deleting last user (tests fail) * tests, allow activating the hidden user when there is no active user * hide logs Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> * comment Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> * comment Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> --------- Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> --- src/Simplex/Chat.hs | 25 +++++++++------ src/Simplex/Chat/View.hs | 4 ++- tests/ChatClient.hs | 4 +-- tests/ChatTests/Direct.hs | 67 ++++++++++++++++++++++++--------------- 4 files changed, 61 insertions(+), 39 deletions(-) diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index dbccfbdfc..6b619c5bd 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -473,12 +473,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 @@ -2300,11 +2302,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 @@ -2327,16 +2332,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 diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs index 4f7c8698b..b0408690a 100644 --- a/src/Simplex/Chat/View.hs +++ b/src/Simplex/Chat/View.hs @@ -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) diff --git a/tests/ChatClient.hs b/tests/ChatClient.hs index 74e29cb0b..821d7b032 100644 --- a/tests/ChatClient.hs +++ b/tests/ChatClient.hs @@ -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 diff --git a/tests/ChatTests/Direct.hs b/tests/ChatTests/Direct.hs index 7d299e296..64fa6ff3b 100644 --- a/tests/ChatTests/Direct.hs +++ b/tests/ChatTests/Direct.hs @@ -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 "/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:") From 6ba3100d348e23549245e2d435fa9815108584e4 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Wed, 20 Dec 2023 06:38:39 +0000 Subject: [PATCH 11/13] core: batch sending messages (#3566) * core: batch sending messages * batch without iorefs (#3573) * one-pass * simplexmq * simplexmq * simplexmq * simplexmq * revert change to ios project file * refactor * simplify --------- Co-authored-by: Alexander Bondarenko <486682+dpwiz@users.noreply.github.com> --- cabal.project | 2 +- package.yaml | 2 +- scripts/nix/sha256map.nix | 2 +- simplex-chat.cabal | 14 ++--- src/Simplex/Chat.hs | 96 +++++++++++++++++++++------------- src/Simplex/Chat/Controller.hs | 15 ++++++ tests/ChatClient.hs | 1 + 7 files changed, 87 insertions(+), 45 deletions(-) diff --git a/cabal.project b/cabal.project index 873035d7a..e81c21c99 100644 --- a/cabal.project +++ b/cabal.project @@ -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 diff --git a/package.yaml b/package.yaml index af58ce672..65f99a7a7 100644 --- a/package.yaml +++ b/package.yaml @@ -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.* diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index 3733163f4..9f06b6610 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -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"; diff --git a/simplex-chat.cabal b/simplex-chat.cabal index f3918dfec..6462d2600 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -199,7 +199,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.* @@ -259,7 +259,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.* @@ -319,7 +319,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.* @@ -381,7 +381,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.* @@ -442,7 +442,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.* @@ -508,7 +508,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.* @@ -602,7 +602,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.* diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index 6b619c5bd..4e7a1cab9 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -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) @@ -5002,7 +5002,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" @@ -5529,46 +5529,62 @@ directMessage chatMsgEvent = do pure $ strEncode ChatMessage {chatVRange, msgId = Nothing, chatMsgEvent} deliverMessage :: ChatMonad m => Connection -> CMEventTag e -> MsgBody -> MessageId -> m Int64 -deliverMessage conn@Connection {connId} cmEventTag msgBody msgId = do - let msgFlags = MsgFlags {notification = hasNotification cmEventTag} - agentMsgId <- withAgent $ \a -> sendMessage a (aConnId conn) msgFlags msgBody - let sndMsgDelivery = SndMsgDelivery {connId, agentMsgId} - withStore' $ \db -> createSndMsgDelivery db sndMsgDelivery msgId +deliverMessage conn cmEventTag msgBody msgId = + deliverMessages [(conn, cmEventTag, msgBody, msgId)] >>= \case + [r] -> liftEither r + rs -> throwChatError $ CEInternalError $ "deliverMessage: expected 1 result, got " <> show (length rs) + +deliverMessages :: ChatMonad' m => [(Connection, CMEventTag e, 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, cmEvTag, msgBody, _msgId) -> (aConnId conn, msgFlags cmEvTag, msgBody)) msgReqs + msgFlags cmEvTag = MsgFlags {notification = hasNotification cmEvTag} + prepareBatch req = bimap (`ChatErrorAgent` Nothing) (req,) + createDelivery :: DB.Connection -> ((Connection, CMEventTag e, 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) $ \GroupMember {memberRole} -> memberRole - rs <- forM recipientMembers $ \m -> - messageMember m msg `catchChatError` (\e -> toView (CRChatError (Just user) e) $> Nothing) - let sentToMembers = catMaybes rs + let tag = toCMEventTag chatMsgEvent + (toSend, pending) = foldr addMember ([], []) recipientMembers + msgReqs = map (\(_, conn) -> (conn, tag, 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 - 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 @@ -5582,6 +5598,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_ + shuffleMembers :: [a] -> (a -> GroupMemberRole) -> IO [a] shuffleMembers ms role = do let (adminMs, otherMs) = partition ((GRAdmin <=) . role) ms diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index 8446c15a8..70e0cc64f 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -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 @@ -1287,12 +1288,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) diff --git a/tests/ChatClient.hs b/tests/ChatClient.hs index 821d7b032..c32d8002b 100644 --- a/tests/ChatClient.hs +++ b/tests/ChatClient.hs @@ -353,6 +353,7 @@ serverCfg = serverStatsBackupFile = Nothing, smpServerVRange = supportedSMPServerVRange, transportConfig = defaultTransportServerConfig, + smpHandshakeTimeout = 1000000, controlPort = Nothing } From 4a4d470859e86b44ebf61447a505957c54ffcf5e Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Thu, 21 Dec 2023 02:00:44 +0800 Subject: [PATCH 12/13] android, desktop: try-catch composables (#3575) * android, desktop: try-catch composables * test * better catching on Android * more try-catch'es * Revert "test" This reverts commit adaf92b116fd8453d44cd401055fb0904f41f23c. * more try-catch'es * unneeded imports --- .../main/java/chat/simplex/app/SimplexApp.kt | 17 ++++++ .../simplex/common/platform/UI.android.kt | 34 ++++++----- .../kotlin/chat/simplex/common/App.kt | 8 ++- .../simplex/common/views/chat/ChatView.kt | 6 +- .../views/chat/item/CIBrokenComposableView.kt | 18 ++++++ .../views/chatlist/ChatListNavLinkView.kt | 60 ++++++++++++++++--- .../common/views/chatlist/ChatListView.kt | 26 ++++++-- .../common/views/chatlist/ShareListView.kt | 10 ++-- .../simplex/common/views/helpers/Utils.kt | 22 +++++++ .../commonMain/resources/MR/base/strings.xml | 3 + .../kotlin/chat/simplex/common/DesktopApp.kt | 1 + 11 files changed, 169 insertions(+), 36 deletions(-) create mode 100644 apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIBrokenComposableView.kt diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexApp.kt b/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexApp.kt index a345e6e48..e3f4e69bd 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexApp.kt +++ b/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexApp.kt @@ -1,6 +1,8 @@ package chat.simplex.app import android.app.Application +import android.os.Handler +import android.os.Looper import chat.simplex.common.platform.Log import androidx.lifecycle.* import androidx.work.* @@ -35,6 +37,21 @@ class SimplexApp: Application(), LifecycleEventObserver { return } else { registerGlobalErrorHandler() + Handler(Looper.getMainLooper()).post { + while (true) { + try { + Looper.loop() + } catch (e: Throwable) { + if (e.message != null && e.message!!.startsWith("Unable to start activity")) { + android.os.Process.killProcess(android.os.Process.myPid()) + break + } else { + // Send it to our exception handled because it will not get the exception otherwise + Thread.getDefaultUncaughtExceptionHandler()?.uncaughtException(Looper.getMainLooper().thread, e) + } + } + } + } } context = this initHaskell() diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/UI.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/UI.android.kt index 96bb73911..371c14013 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/UI.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/UI.android.kt @@ -4,7 +4,7 @@ import android.app.Activity import android.content.Context import android.content.pm.ActivityInfo import android.graphics.Rect -import android.os.Build +import android.os.* import android.view.* import android.view.inputmethod.InputMethodManager import android.widget.Toast @@ -12,7 +12,6 @@ import androidx.activity.compose.setContent import androidx.compose.runtime.* import androidx.compose.ui.platform.LocalView import chat.simplex.common.AppScreen -import chat.simplex.common.ui.theme.SimpleXTheme import chat.simplex.common.views.helpers.* import androidx.compose.ui.platform.LocalContext as LocalContext1 import chat.simplex.res.MR @@ -79,6 +78,7 @@ actual fun androidIsFinishingMainActivity(): Boolean = (mainActivity.get()?.isFi actual class GlobalExceptionsHandler: Thread.UncaughtExceptionHandler { actual override fun uncaughtException(thread: Thread, e: Throwable) { Log.e(TAG, "App crashed, thread name: " + thread.name + ", exception: " + e.stackTraceToString()) + includeMoreFailedComposables() if (ModalManager.start.hasModalsOpen()) { ModalManager.start.closeModal() } else if (chatModel.chatId.value != null) { @@ -93,19 +93,25 @@ actual class GlobalExceptionsHandler: Thread.UncaughtExceptionHandler { chatModel.callManager.endCall(it) } } - AlertManager.shared.showAlertMsg( - title = generalGetString(MR.strings.app_was_crashed), - text = e.stackTraceToString() - ) - //mainActivity.get()?.recreate() - mainActivity.get()?.apply { - window - ?.decorView - ?.findViewById(android.R.id.content) - ?.removeViewAt(0) - setContent { - AppScreen() + if (thread.name == "main") { + mainActivity.get()?.recreate() + } else { + mainActivity.get()?.apply { + window + ?.decorView + ?.findViewById(android.R.id.content) + ?.removeViewAt(0) + setContent { + AppScreen() + } } } + // Wait until activity recreates to prevent showing two alerts (in case `main` was crashed) + Handler(Looper.getMainLooper()).post { + AlertManager.shared.showAlertMsg( + title = generalGetString(MR.strings.app_was_crashed), + text = e.stackTraceToString() + ) + } } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/App.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/App.kt index 0082972c7..d457eb57a 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/App.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/App.kt @@ -332,9 +332,11 @@ fun DesktopScreen(settingsState: SettingsViewState) { ) } VerticalDivider(Modifier.padding(start = DEFAULT_START_MODAL_WIDTH)) - UserPicker(chatModel, userPickerState) { - scope.launch { if (scaffoldState.drawerState.isOpen) scaffoldState.drawerState.close() else scaffoldState.drawerState.open() } - userPickerState.value = AnimatedViewState.GONE + tryOrShowError("UserPicker", error = {}) { + UserPicker(chatModel, userPickerState) { + scope.launch { if (scaffoldState.drawerState.isOpen) scaffoldState.drawerState.close() else scaffoldState.drawerState.open() } + userPickerState.value = AnimatedViewState.GONE + } } ModalManager.fullscreen.showInView() } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt index 8eee43035..ebec780df 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt @@ -900,7 +900,11 @@ fun BoxWithConstraintsScope.ChatItemsList( @Composable fun ChatItemViewShortHand(cItem: ChatItem, range: IntRange?) { - ChatItemView(chat.chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, revealed = revealed, range = range, deleteMessage = deleteMessage, deleteMessages = deleteMessages, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = joinGroup, acceptCall = acceptCall, acceptFeature = acceptFeature, openDirectChat = openDirectChat, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, setReaction = setReaction, showItemDetails = showItemDetails, developerTools = developerTools) + tryOrShowError("${cItem.id}ChatItem", error = { + CIBrokenComposableView(if (cItem.chatDir.sent) Alignment.CenterEnd else Alignment.CenterStart) + }) { + ChatItemView(chat.chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, revealed = revealed, range = range, deleteMessage = deleteMessage, deleteMessages = deleteMessages, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = joinGroup, acceptCall = acceptCall, acceptFeature = acceptFeature, openDirectChat = openDirectChat, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, setReaction = setReaction, showItemDetails = showItemDetails, developerTools = developerTools) + } } @Composable diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIBrokenComposableView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIBrokenComposableView.kt new file mode 100644 index 000000000..d49f8526d --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIBrokenComposableView.kt @@ -0,0 +1,18 @@ +package chat.simplex.common.views.chat.item + +import androidx.compose.foundation.layout.* +import androidx.compose.material.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import dev.icerock.moko.resources.compose.stringResource +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.unit.dp +import chat.simplex.res.MR + +@Composable +fun CIBrokenComposableView(alignment: Alignment) { + Box(Modifier.fillMaxWidth().padding(horizontal = 10.dp, vertical = 6.dp), contentAlignment = alignment) { + Text(stringResource(MR.strings.error_showing_message), color = MaterialTheme.colors.error, fontStyle = FontStyle.Italic) + } +} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt index 9ae0da2a3..8d5446aa5 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt @@ -14,6 +14,7 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.desktop.ui.tooling.preview.Preview import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @@ -61,9 +62,17 @@ fun ChatListNavLinkView(chat: Chat, chatModel: ChatModel) { is ChatInfo.Direct -> { val contactNetworkStatus = chatModel.contactNetworkStatus(chat.chatInfo.contact) ChatListNavLinkLayout( - chatLinkPreview = { ChatPreviewView(chat, showChatPreviews, chatModel.draft.value, chatModel.draftChatId.value, chatModel.currentUser.value?.profile?.displayName, contactNetworkStatus, stopped, linkMode, inProgress = false, progressByTimeout = false) }, + chatLinkPreview = { + tryOrShowError("${chat.id}ChatListNavLink", error = { ErrorChatListItem() }) { + ChatPreviewView(chat, showChatPreviews, chatModel.draft.value, chatModel.draftChatId.value, chatModel.currentUser.value?.profile?.displayName, contactNetworkStatus, stopped, linkMode, inProgress = false, progressByTimeout = false) + } + }, click = { directChatAction(chat.remoteHostId, chat.chatInfo.contact, chatModel) }, - dropdownMenuItems = { ContactMenuItems(chat, chat.chatInfo.contact, chatModel, showMenu, showMarkRead) }, + dropdownMenuItems = { + tryOrShowError("${chat.id}ChatListNavLinkDropdown", error = {}) { + ContactMenuItems(chat, chat.chatInfo.contact, chatModel, showMenu, showMarkRead) + } + }, showMenu, stopped, selectedChat @@ -71,25 +80,45 @@ fun ChatListNavLinkView(chat: Chat, chatModel: ChatModel) { } is ChatInfo.Group -> ChatListNavLinkLayout( - chatLinkPreview = { ChatPreviewView(chat, showChatPreviews, chatModel.draft.value, chatModel.draftChatId.value, chatModel.currentUser.value?.profile?.displayName, null, stopped, linkMode, inProgress.value, progressByTimeout) }, + chatLinkPreview = { + tryOrShowError("${chat.id}ChatListNavLink", error = { ErrorChatListItem() }) { + ChatPreviewView(chat, showChatPreviews, chatModel.draft.value, chatModel.draftChatId.value, chatModel.currentUser.value?.profile?.displayName, null, stopped, linkMode, inProgress.value, progressByTimeout) + } + }, click = { if (!inProgress.value) groupChatAction(chat.remoteHostId, chat.chatInfo.groupInfo, chatModel, inProgress) }, - dropdownMenuItems = { GroupMenuItems(chat, chat.chatInfo.groupInfo, chatModel, showMenu, inProgress, showMarkRead) }, + dropdownMenuItems = { + tryOrShowError("${chat.id}ChatListNavLinkDropdown", error = {}) { + GroupMenuItems(chat, chat.chatInfo.groupInfo, chatModel, showMenu, inProgress, showMarkRead) + } + }, showMenu, stopped, selectedChat ) is ChatInfo.ContactRequest -> ChatListNavLinkLayout( - chatLinkPreview = { ContactRequestView(chat.chatInfo) }, + chatLinkPreview = { + tryOrShowError("${chat.id}ChatListNavLink", error = { ErrorChatListItem() }) { + ContactRequestView(chat.chatInfo) + } + }, click = { contactRequestAlertDialog(chat.remoteHostId, chat.chatInfo, chatModel) }, - dropdownMenuItems = { ContactRequestMenuItems(chat.remoteHostId, chat.chatInfo, chatModel, showMenu) }, + dropdownMenuItems = { + tryOrShowError("${chat.id}ChatListNavLinkDropdown", error = {}) { + ContactRequestMenuItems(chat.remoteHostId, chat.chatInfo, chatModel, showMenu) + } + }, showMenu, stopped, selectedChat ) is ChatInfo.ContactConnection -> ChatListNavLinkLayout( - chatLinkPreview = { ContactConnectionView(chat.chatInfo.contactConnection) }, + chatLinkPreview = { + tryOrShowError("${chat.id}ChatListNavLink", error = { ErrorChatListItem() }) { + ContactConnectionView(chat.chatInfo.contactConnection) + } + }, click = { ModalManager.center.closeModals() ModalManager.end.closeModals() @@ -97,7 +126,11 @@ fun ChatListNavLinkView(chat: Chat, chatModel: ChatModel) { ContactConnectionInfoView(chatModel, chat.remoteHostId, chat.chatInfo.contactConnection.connReqInv, chat.chatInfo.contactConnection, false, close) } }, - dropdownMenuItems = { ContactConnectionMenuItems(chat.remoteHostId, chat.chatInfo, chatModel, showMenu) }, + dropdownMenuItems = { + tryOrShowError("${chat.id}ChatListNavLinkDropdown", error = {}) { + ContactConnectionMenuItems(chat.remoteHostId, chat.chatInfo, chatModel, showMenu) + } + }, showMenu, stopped, selectedChat @@ -105,7 +138,9 @@ fun ChatListNavLinkView(chat: Chat, chatModel: ChatModel) { is ChatInfo.InvalidJSON -> ChatListNavLinkLayout( chatLinkPreview = { - InvalidDataView() + tryOrShowError("${chat.id}ChatListNavLink", error = { ErrorChatListItem() }) { + InvalidDataView() + } }, click = { ModalManager.end.closeModals() @@ -119,6 +154,13 @@ fun ChatListNavLinkView(chat: Chat, chatModel: ChatModel) { } } +@Composable +private fun ErrorChatListItem() { + Box(Modifier.fillMaxWidth().padding(horizontal = 10.dp, vertical = 6.dp)) { + Text(stringResource(MR.strings.error_showing_content), color = MaterialTheme.colors.error, fontStyle = FontStyle.Italic) + } +} + fun directChatAction(rhId: Long?, contact: Contact, chatModel: ChatModel) { when { contact.activeConn == null && contact.profile.contactLink != null -> askCurrentOrIncognitoProfileConnectContactViaAddress(chatModel, rhId, contact, close = null, openChat = true) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt index 18252d0e2..cf12727d7 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt @@ -11,6 +11,7 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.* +import androidx.compose.ui.text.font.FontStyle import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.text.font.FontWeight @@ -64,7 +65,11 @@ fun ChatListView(chatModel: ChatModel, settingsState: SettingsViewState, setPerf val (userPickerState, scaffoldState ) = settingsState Scaffold(topBar = { Box(Modifier.padding(end = endPadding)) { ChatListToolbar(chatModel, scaffoldState.drawerState, userPickerState, stopped) { searchInList = it.trim() } } }, scaffoldState = scaffoldState, - drawerContent = { SettingsView(chatModel, setPerformLA, scaffoldState.drawerState) }, + drawerContent = { + tryOrShowError("Settings", error = { ErrorSettingsView() }) { + SettingsView(chatModel, setPerformLA, scaffoldState.drawerState) + } + }, drawerScrimColor = MaterialTheme.colors.onSurface.copy(alpha = if (isInDarkTheme()) 0.16f else 0.32f), drawerGesturesEnabled = appPlatform.isAndroid, floatingActionButton = { @@ -111,12 +116,16 @@ fun ChatListView(chatModel: ChatModel, settingsState: SettingsViewState, setPerf if (searchInList.isEmpty()) { DesktopActiveCallOverlayLayout(newChatSheetState) // TODO disable this button and sheet for the duration of the switch - NewChatSheet(chatModel, newChatSheetState, stopped, hideNewChatSheet) + tryOrShowError("NewChatSheet", error = {}) { + NewChatSheet(chatModel, newChatSheetState, stopped, hideNewChatSheet) + } } if (appPlatform.isAndroid) { - UserPicker(chatModel, userPickerState) { - scope.launch { if (scaffoldState.drawerState.isOpen) scaffoldState.drawerState.close() else scaffoldState.drawerState.open() } - userPickerState.value = AnimatedViewState.GONE + tryOrShowError("UserPicker", error = {}) { + UserPicker(chatModel, userPickerState) { + scope.launch { if (scaffoldState.drawerState.isOpen) scaffoldState.drawerState.close() else scaffoldState.drawerState.open() } + userPickerState.value = AnimatedViewState.GONE + } } } } @@ -303,6 +312,13 @@ fun connectIfOpenedViaUri(rhId: Long?, uri: URI, chatModel: ChatModel) { } } +@Composable +private fun ErrorSettingsView() { + Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + Text(generalGetString(MR.strings.error_showing_content), color = MaterialTheme.colors.error, fontStyle = FontStyle.Italic) + } +} + private var lazyListState = 0 to 0 @Composable diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ShareListView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ShareListView.kt index 8338d2960..ac8331007 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ShareListView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ShareListView.kt @@ -47,10 +47,12 @@ fun ShareListView(chatModel: ChatModel, settingsState: SettingsViewState, stoppe } } if (appPlatform.isAndroid) { - UserPicker(chatModel, userPickerState, showSettings = false, showCancel = true, cancelClicked = { - chatModel.sharedContent.value = null - userPickerState.value = AnimatedViewState.GONE - }) + tryOrShowError("UserPicker", error = {}) { + UserPicker(chatModel, userPickerState, showSettings = false, showCancel = true, cancelClicked = { + chatModel.sharedContent.value = null + userPickerState.value = AnimatedViewState.GONE + }) + } } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Utils.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Utils.kt index 0a0ef17c4..9a81b9f9d 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Utils.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Utils.kt @@ -390,6 +390,28 @@ fun IntSize.Companion.Saver(): Saver = Saver( restore = { IntSize(it.first, it.second) } ) +private var lastExecutedComposables = HashSet() +private val failedComposables = HashSet() + +@Composable +fun tryOrShowError(key: Any = Exception().stackTraceToString().lines()[2], error: @Composable () -> Unit = {}, content: @Composable () -> Unit) { + if (!failedComposables.contains(key)) { + lastExecutedComposables.add(key) + content() + lastExecutedComposables.remove(key) + } else { + error() + } +} + +fun includeMoreFailedComposables() { + lastExecutedComposables.forEach { + failedComposables.add(it) + Log.i(TAG, "Added composable key as failed: $it") + } + lastExecutedComposables.clear() +} + @Composable fun DisposableEffectOnGone(always: () -> Unit = {}, whenDispose: () -> Unit = {}, whenGone: () -> Unit) { DisposableEffect(Unit) { diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml index e0b8f130d..7ee86c2f5 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -45,6 +45,9 @@ moderated invalid chat invalid data + error showing message + error showing content + Decryption error Encryption re-negotiation error diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/DesktopApp.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/DesktopApp.kt index 12bead366..57371e25a 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/DesktopApp.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/DesktopApp.kt @@ -45,6 +45,7 @@ fun showApp() { Log.e(TAG, "App crashed, thread name: " + Thread.currentThread().name + ", exception: " + e.stackTraceToString()) window.dispatchEvent(WindowEvent(window, WindowEvent.WINDOW_CLOSING)) closedByError.value = true + includeMoreFailedComposables() // If the left side of screen has open modal, it's probably caused the crash if (ModalManager.start.hasModalsOpen()) { ModalManager.start.closeModal() From 7bcda7e54b8cb3b19bf09d58a62bb7714d7757d9 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Thu, 21 Dec 2023 00:42:40 +0000 Subject: [PATCH 13/13] core: use ChaChaDRG as the source of randomness (#3551) * core: use ChaChaDRG as the source of randomness * do not use entropy directly * dont use RNG from agent * simplexmq * update iOS --- apps/ios/Shared/Views/Call/WebRTCClient.swift | 3 +- apps/ios/SimpleXChat/CryptoFile.swift | 4 +- apps/ios/SimpleXChat/SimpleX.h | 6 +-- cabal.project | 2 +- scripts/nix/sha256map.nix | 2 +- src/Simplex/Chat.hs | 35 ++++++++-------- src/Simplex/Chat/Controller.hs | 2 +- src/Simplex/Chat/Mobile.hs | 6 +-- src/Simplex/Chat/Mobile/File.hs | 31 ++++++++------ src/Simplex/Chat/Mobile/WebRTC.hs | 15 ++++--- src/Simplex/Chat/Remote.hs | 4 +- src/Simplex/Chat/Remote/Protocol.hs | 4 +- src/Simplex/Chat/Remote/Transport.hs | 2 +- src/Simplex/Chat/Store/Shared.hs | 8 ++-- tests/ChatTests/Files.hs | 2 +- tests/MobileTests.hs | 40 ++++++++++++------- tests/RemoteTests.hs | 10 ----- tests/Test.hs | 7 ++-- tests/WebRTCTests.hs | 31 +++++++++----- 19 files changed, 120 insertions(+), 94 deletions(-) diff --git a/apps/ios/Shared/Views/Call/WebRTCClient.swift b/apps/ios/Shared/Views/Call/WebRTCClient.swift index acb459938..933a3c745 100644 --- a/apps/ios/Shared/Views/Call/WebRTCClient.swift +++ b/apps/ios/Shared/Views/Call/WebRTCClient.swift @@ -18,6 +18,7 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg }() private static let ivTagBytes: Int = 28 private static let enableEncryption: Bool = true + private var chat_ctrl = getChatCtrl() struct Call { var connection: RTCPeerConnection @@ -308,7 +309,7 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg memcpy(pointer, (unencrypted as NSData).bytes, unencrypted.count) let isKeyFrame = unencrypted[0] & 1 == 0 let clearTextBytesSize = mediaType.rawValue == 0 ? 1 : isKeyFrame ? 10 : 3 - logCrypto("encrypt", chat_encrypt_media(&key, pointer.advanced(by: clearTextBytesSize), Int32(unencrypted.count + WebRTCClient.ivTagBytes - clearTextBytesSize))) + logCrypto("encrypt", chat_encrypt_media(chat_ctrl, &key, pointer.advanced(by: clearTextBytesSize), Int32(unencrypted.count + WebRTCClient.ivTagBytes - clearTextBytesSize))) return Data(bytes: pointer, count: unencrypted.count + WebRTCClient.ivTagBytes) } else { return nil diff --git a/apps/ios/SimpleXChat/CryptoFile.swift b/apps/ios/SimpleXChat/CryptoFile.swift index dcb2be9ae..0e539ba97 100644 --- a/apps/ios/SimpleXChat/CryptoFile.swift +++ b/apps/ios/SimpleXChat/CryptoFile.swift @@ -17,7 +17,7 @@ public func writeCryptoFile(path: String, data: Data) throws -> CryptoFileArgs { let ptr: UnsafeMutableRawPointer = malloc(data.count) memcpy(ptr, (data as NSData).bytes, data.count) var cPath = path.cString(using: .utf8)! - let cjson = chat_write_file(&cPath, ptr, Int32(data.count))! + let cjson = chat_write_file(getChatCtrl(), &cPath, ptr, Int32(data.count))! let d = fromCString(cjson).data(using: .utf8)! switch try jsonDecoder.decode(WriteFileResult.self, from: d) { case let .result(cfArgs): return cfArgs @@ -50,7 +50,7 @@ public func readCryptoFile(path: String, cryptoArgs: CryptoFileArgs) throws -> D public func encryptCryptoFile(fromPath: String, toPath: String) throws -> CryptoFileArgs { var cFromPath = fromPath.cString(using: .utf8)! var cToPath = toPath.cString(using: .utf8)! - let cjson = chat_encrypt_file(&cFromPath, &cToPath)! + let cjson = chat_encrypt_file(getChatCtrl(), &cFromPath, &cToPath)! let d = fromCString(cjson).data(using: .utf8)! switch try jsonDecoder.decode(WriteFileResult.self, from: d) { case let .result(cfArgs): return cfArgs diff --git a/apps/ios/SimpleXChat/SimpleX.h b/apps/ios/SimpleXChat/SimpleX.h index 6e37a5177..909d76a76 100644 --- a/apps/ios/SimpleXChat/SimpleX.h +++ b/apps/ios/SimpleXChat/SimpleX.h @@ -25,11 +25,11 @@ extern char *chat_parse_markdown(char *str); extern char *chat_parse_server(char *str); extern char *chat_password_hash(char *pwd, char *salt); extern char *chat_valid_name(char *name); -extern char *chat_encrypt_media(char *key, char *frame, int len); +extern char *chat_encrypt_media(chat_ctrl ctl, char *key, char *frame, int len); extern char *chat_decrypt_media(char *key, char *frame, int len); // chat_write_file returns null-terminated string with JSON of WriteFileResult -extern char *chat_write_file(char *path, char *data, int len); +extern char *chat_write_file(chat_ctrl ctl, char *path, char *data, int len); // chat_read_file returns a buffer with: // result status (1 byte), then if @@ -38,7 +38,7 @@ extern char *chat_write_file(char *path, char *data, int len); extern char *chat_read_file(char *path, char *key, char *nonce); // chat_encrypt_file returns null-terminated string with JSON of WriteFileResult -extern char *chat_encrypt_file(char *fromPath, char *toPath); +extern char *chat_encrypt_file(chat_ctrl ctl, char *fromPath, char *toPath); // chat_decrypt_file returns null-terminated string with the error message extern char *chat_decrypt_file(char *fromPath, char *key, char *nonce, char *toPath); diff --git a/cabal.project b/cabal.project index e81c21c99..1ff8aacd7 100644 --- a/cabal.project +++ b/cabal.project @@ -14,7 +14,7 @@ constraints: zip +disable-bzip2 +disable-zstd source-repository-package type: git location: https://github.com/simplex-chat/simplexmq.git - tag: 8c250ebe19f56dd7d53572d984e8016cb0e4d658 + tag: 13a60d1d3944aa175311563e661161e759b92563 source-repository-package type: git diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index 9f06b6610..595d40c4e 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,5 +1,5 @@ { - "https://github.com/simplex-chat/simplexmq.git"."8c250ebe19f56dd7d53572d984e8016cb0e4d658" = "080rw86yncf1h3zr5a8y65cndihq6f3ji43vxrdhr2mrb75vmw8m"; + "https://github.com/simplex-chat/simplexmq.git"."13a60d1d3944aa175311563e661161e759b92563" = "08mvqrbjfnq7c6mhkj4hhy4cxn0cj21n49lqzh67ani71g2g1xwa"; "https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38"; "https://github.com/simplex-chat/direct-sqlcipher.git"."f814ee68b16a9447fbb467ccc8f29bdd3546bfd9" = "1ql13f4kfwkbaq7nygkxgw84213i0zm7c1a8hwvramayxl38dq5d"; "https://github.com/simplex-chat/sqlcipher-simple.git"."a46bd361a19376c5211f1058908fc0ae6bf42446" = "1z0r78d8f0812kxbgsm735qf6xx8lvaz27k1a0b4a2m0sshpd5gl"; diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index 4e7a1cab9..8bce204f5 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -22,7 +22,6 @@ import Control.Monad import Control.Monad.Except import Control.Monad.IO.Unlift import Control.Monad.Reader -import Crypto.Random (drgNew) import qualified Data.Aeson as J import Data.Attoparsec.ByteString.Char8 (Parser) import qualified Data.Attoparsec.ByteString.Char8 as A @@ -208,7 +207,7 @@ newChatController ChatDatabase {chatStore, agentStore} user cfg@ChatConfig {agen servers <- agentServers config smpAgent <- getSMPAgentClient aCfg {tbqSize} servers agentStore agentAsync <- newTVarIO Nothing - idsDrg <- newTVarIO =<< liftIO drgNew + random <- liftIO C.newRandom inputQ <- newTBQueueIO tbqSize outputQ <- newTBQueueIO tbqSize connNetworkStatuses <- atomically TM.empty @@ -243,7 +242,7 @@ newChatController ChatDatabase {chatStore, agentStore} user cfg@ChatConfig {agen agentAsync, chatStore, chatStoreChanged, - idsDrg, + random, inputQ, outputQ, connNetworkStatuses, @@ -1077,8 +1076,9 @@ processChatCommand = \case then do calls <- asks currentCalls withChatLock "sendCallInvitation" $ do - callId <- CallId <$> drgRandomBytes 16 - dhKeyPair <- if encryptedCall callType then Just <$> liftIO C.generateKeyPair' else pure Nothing + g <- asks random + callId <- atomically $ CallId <$> C.randomBytes 16 g + dhKeyPair <- atomically $ if encryptedCall callType then Just <$> C.generateKeyPair g else pure Nothing let invitation = CallInvitation {callType, callDhPubKey = fst <$> dhKeyPair} callState = CallInvitationSent {localCallType = callType, localDhPrivKey = snd <$> dhKeyPair} (msg, _) <- sendDirectContactMessage ct (XCallInv callId invitation) @@ -1600,7 +1600,7 @@ processChatCommand = \case processChatCommand $ APIChatItemReaction chatRef chatItemId add reaction APINewGroup userId incognito gProfile@GroupProfile {displayName} -> withUserId userId $ \user -> do checkValidName displayName - gVar <- asks idsDrg + gVar <- asks random -- [incognito] generate incognito profile for group membership incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing groupInfo <- withStore $ \db -> createNewGroup db gVar user gProfile incognitoProfile @@ -1621,7 +1621,7 @@ processChatCommand = \case let sendInvitation = sendGrpInvitation user contact gInfo case contactMember contact members of Nothing -> do - gVar <- asks idsDrg + gVar <- asks random subMode <- chatReadVar subscriptionMode (agentConnId, cReq) <- withAgent $ \a -> createConnection a (aUserId user) True SCMInvitation Nothing subMode member <- withStore $ \db -> createNewContactMember db gVar user gInfo contact memRole agentConnId cReq subMode @@ -1884,7 +1884,7 @@ processChatCommand = \case SetFileToReceive fileId encrypted_ -> withUser $ \_ -> do withChatLock "setFileToReceive" . procCmd $ do encrypt <- (`fromMaybe` encrypted_) <$> chatReadVar encryptLocalFiles - cfArgs <- if encrypt then Just <$> liftIO CF.randomArgs else pure Nothing + cfArgs <- if encrypt then Just <$> (atomically . CF.randomArgs =<< asks random) else pure Nothing withStore' $ \db -> setRcvFileToReceive db fileId cfArgs ok_ CancelFile fileId -> withUser $ \user@User {userId} -> @@ -2030,7 +2030,7 @@ processChatCommand = \case -- in View.hs `r'` should be defined as `id` in this case -- procCmd :: m ChatResponse -> m ChatResponse -- procCmd action = do - -- ChatController {chatLock = l, smpAgent = a, outputQ = q, idsDrg = gVar} <- ask + -- ChatController {chatLock = l, smpAgent = a, outputQ = q, random = gVar} <- ask -- corrId <- liftIO $ SMP.CorrId <$> randomBytes gVar 8 -- void . forkIO $ -- withAgentLock a . withLock l name $ @@ -2296,7 +2296,7 @@ processChatCommand = \case then pure Nothing else Just . addUTCTime (realToFrac ttl) <$> liftIO getCurrentTime drgRandomBytes :: Int -> m ByteString - drgRandomBytes n = asks idsDrg >>= liftIO . (`randomBytes` n) + drgRandomBytes n = asks random >>= atomically . C.randomBytes n privateGetUser :: UserId -> m User privateGetUser userId = tryChatError (withStore (`getUser` userId)) >>= \case @@ -2571,7 +2571,7 @@ toFSFilePath f = setFileToEncrypt :: ChatMonad m => RcvFileTransfer -> m RcvFileTransfer setFileToEncrypt ft@RcvFileTransfer {fileId} = do - cfArgs <- liftIO CF.randomArgs + cfArgs <- atomically . CF.randomArgs =<< asks random withStore' $ \db -> setFileCryptoArgs db fileId cfArgs pure (ft :: RcvFileTransfer) {cryptoArgs = Just cfArgs} @@ -2726,7 +2726,7 @@ acceptGroupJoinRequestAsync ucr@UserContactRequest {agentInvitationId = AgentInvId invId} gLinkMemRole incognitoProfile = do - gVar <- asks idsDrg + gVar <- asks random (groupMemberId, memberId) <- withStore $ \db -> createAcceptedMember db gVar user gInfo ucr gLinkMemRole let Profile {displayName} = profileToSendOnAccept user incognitoProfile GroupMember {memberRole = userRole, memberId = userMemberId} = membership @@ -3407,7 +3407,7 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do groupInfo <- withStore $ \db -> getGroupInfo db user groupId subMode <- chatReadVar subscriptionMode groupConnIds <- createAgentConnectionAsync user CFCreateConnGrpInv True SCMInvitation subMode - gVar <- asks idsDrg + gVar <- asks random withStore $ \db -> createNewContactMemberAsync db gVar user groupInfo ct gLinkMemRole groupConnIds (fromJVersionRange peerChatVRange) subMode Just (gInfo, m@GroupMember {activeConn}) -> when (maybe False ((== ConnReady) . connStatus) activeConn) $ do @@ -4049,7 +4049,7 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do probeMatchingContactsAndMembers :: Contact -> IncognitoEnabled -> Bool -> m () probeMatchingContactsAndMembers ct connectedIncognito doProbeContacts = do - gVar <- asks idsDrg + gVar <- asks random contactMerge <- readTVarIO =<< asks contactMergeEnabled if contactMerge && not connectedIncognito then do @@ -4073,7 +4073,7 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do probeMatchingMemberContact :: GroupMember -> IncognitoEnabled -> m () probeMatchingMemberContact GroupMember {activeConn = Nothing} _ = pure () probeMatchingMemberContact m@GroupMember {groupId, activeConn = Just conn} connectedIncognito = do - gVar <- asks idsDrg + gVar <- asks random contactMerge <- readTVarIO =<< asks contactMergeEnabled if contactMerge && not connectedIncognito then do @@ -4774,7 +4774,8 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do checkIntegrityCreateItem (CDDirectRcv ct) msgMeta if featureAllowed SCFCalls forContact ct then do - dhKeyPair <- if encryptedCall callType then Just <$> liftIO C.generateKeyPair' else pure Nothing + g <- asks random + dhKeyPair <- atomically $ if encryptedCall callType then Just <$> C.generateKeyPair g else pure Nothing ci <- saveCallItem CISCallPending let sharedKey = C.Key . C.dhBytes' <$> (C.dh' <$> callDhPubKey <*> (snd <$> dhKeyPair)) callState = CallInvitationReceived {peerCallType = callType, localDhPubKey = fst <$> dhKeyPair, sharedKey} @@ -5517,7 +5518,7 @@ sendDirectMessage conn chatMsgEvent connOrGroupId = do createSndMessage :: (MsgEncodingI e, ChatMonad m) => ChatMsgEvent e -> ConnOrGroupId -> m SndMessage createSndMessage chatMsgEvent connOrGroupId = do - gVar <- asks idsDrg + gVar <- asks random ChatConfig {chatVRange} <- asks config withStore $ \db -> createNewSndMessage db gVar connOrGroupId $ \sharedMsgId -> let msgBody = strEncode ChatMessage {chatVRange, msgId = Just sharedMsgId, chatMsgEvent} diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index 70e0cc64f..b198cccbf 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -180,7 +180,7 @@ data ChatController = ChatController agentAsync :: TVar (Maybe (Async (), Maybe (Async ()))), chatStore :: SQLiteStore, chatStoreChanged :: TVar Bool, -- if True, chat should be fully restarted - idsDrg :: TVar ChaChaDRG, + random :: TVar ChaChaDRG, inputQ :: TBQueue String, outputQ :: TBQueue (Maybe CorrId, Maybe RemoteHostId, ChatResponse), connNetworkStatuses :: TMap AgentConnId NetworkStatus, diff --git a/src/Simplex/Chat/Mobile.hs b/src/Simplex/Chat/Mobile.hs index a7f032c75..6540352a3 100644 --- a/src/Simplex/Chat/Mobile.hs +++ b/src/Simplex/Chat/Mobile.hs @@ -94,15 +94,15 @@ foreign export ccall "chat_password_hash" cChatPasswordHash :: CString -> CStrin foreign export ccall "chat_valid_name" cChatValidName :: CString -> IO CString -foreign export ccall "chat_encrypt_media" cChatEncryptMedia :: CString -> Ptr Word8 -> CInt -> IO CString +foreign export ccall "chat_encrypt_media" cChatEncryptMedia :: StablePtr ChatController -> CString -> Ptr Word8 -> CInt -> IO CString foreign export ccall "chat_decrypt_media" cChatDecryptMedia :: CString -> Ptr Word8 -> CInt -> IO CString -foreign export ccall "chat_write_file" cChatWriteFile :: CString -> Ptr Word8 -> CInt -> IO CJSONString +foreign export ccall "chat_write_file" cChatWriteFile :: StablePtr ChatController -> CString -> Ptr Word8 -> CInt -> IO CJSONString foreign export ccall "chat_read_file" cChatReadFile :: CString -> CString -> CString -> IO (Ptr Word8) -foreign export ccall "chat_encrypt_file" cChatEncryptFile :: CString -> CString -> IO CJSONString +foreign export ccall "chat_encrypt_file" cChatEncryptFile :: StablePtr ChatController -> CString -> CString -> IO CJSONString foreign export ccall "chat_decrypt_file" cChatDecryptFile :: CString -> CString -> CString -> CString -> IO CString diff --git a/src/Simplex/Chat/Mobile/File.hs b/src/Simplex/Chat/Mobile/File.hs index 1da64a304..afbb1bc8c 100644 --- a/src/Simplex/Chat/Mobile/File.hs +++ b/src/Simplex/Chat/Mobile/File.hs @@ -1,5 +1,6 @@ {-# LANGUAGE BangPatterns #-} {-# LANGUAGE LambdaCase #-} +{-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TupleSections #-} @@ -31,7 +32,9 @@ import Data.Word (Word32, Word8) import Foreign.C import Foreign.Marshal.Alloc (mallocBytes) import Foreign.Ptr +import Foreign.StablePtr import Foreign.Storable (poke, pokeByteOff) +import Simplex.Chat.Controller (ChatController (..)) import Simplex.Chat.Mobile.Shared import Simplex.Chat.Util (chunkSize, encryptFile) import Simplex.Messaging.Crypto.File (CryptoFile (..), CryptoFileArgs (..), CryptoFileHandle, FTCryptoError (..)) @@ -39,7 +42,7 @@ import qualified Simplex.Messaging.Crypto.File as CF import Simplex.Messaging.Encoding.String import Simplex.Messaging.Parsers (dropPrefix, sumTypeJSON) import Simplex.Messaging.Util (catchAll) -import UnliftIO (Handle, IOMode (..), withFile) +import UnliftIO (Handle, IOMode (..), atomically, withFile) data WriteFileResult = WFResult {cryptoArgs :: CryptoFileArgs} @@ -47,16 +50,17 @@ data WriteFileResult $(JQ.deriveToJSON (sumTypeJSON $ dropPrefix "WF") ''WriteFileResult) -cChatWriteFile :: CString -> Ptr Word8 -> CInt -> IO CJSONString -cChatWriteFile cPath ptr len = do +cChatWriteFile :: StablePtr ChatController -> CString -> Ptr Word8 -> CInt -> IO CJSONString +cChatWriteFile cc cPath ptr len = do + c <- deRefStablePtr cc path <- peekCString cPath s <- getByteString ptr len - r <- chatWriteFile path s + r <- chatWriteFile c path s newCStringFromLazyBS $ J.encode r -chatWriteFile :: FilePath -> ByteString -> IO WriteFileResult -chatWriteFile path s = do - cfArgs <- CF.randomArgs +chatWriteFile :: ChatController -> FilePath -> ByteString -> IO WriteFileResult +chatWriteFile ChatController {random} path s = do + cfArgs <- atomically $ CF.randomArgs random let file = CryptoFile path $ Just cfArgs either WFError (\_ -> WFResult cfArgs) <$> runCatchExceptT (withExceptT show $ CF.writeFile file $ LB.fromStrict s) @@ -87,19 +91,20 @@ chatReadFile path keyStr nonceStr = runCatchExceptT $ do let file = CryptoFile path $ Just $ CFArgs key nonce withExceptT show $ CF.readFile file -cChatEncryptFile :: CString -> CString -> IO CJSONString -cChatEncryptFile cFromPath cToPath = do +cChatEncryptFile :: StablePtr ChatController -> CString -> CString -> IO CJSONString +cChatEncryptFile cc cFromPath cToPath = do + c <- deRefStablePtr cc fromPath <- peekCString cFromPath toPath <- peekCString cToPath - r <- chatEncryptFile fromPath toPath + r <- chatEncryptFile c fromPath toPath newCAString . LB'.unpack $ J.encode r -chatEncryptFile :: FilePath -> FilePath -> IO WriteFileResult -chatEncryptFile fromPath toPath = +chatEncryptFile :: ChatController -> FilePath -> FilePath -> IO WriteFileResult +chatEncryptFile ChatController {random} fromPath toPath = either WFError WFResult <$> runCatchExceptT encrypt where encrypt = do - cfArgs <- liftIO CF.randomArgs + cfArgs <- atomically $ CF.randomArgs random encryptFile fromPath toPath cfArgs pure cfArgs diff --git a/src/Simplex/Chat/Mobile/WebRTC.hs b/src/Simplex/Chat/Mobile/WebRTC.hs index 422cfd5a8..537388b18 100644 --- a/src/Simplex/Chat/Mobile/WebRTC.hs +++ b/src/Simplex/Chat/Mobile/WebRTC.hs @@ -1,4 +1,5 @@ {-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE NamedFieldPuns #-} module Simplex.Chat.Mobile.WebRTC ( cChatEncryptMedia, @@ -21,11 +22,14 @@ import Data.Either (fromLeft) import Data.Word (Word8) import Foreign.C (CInt, CString, newCAString) import Foreign.Ptr (Ptr) +import Foreign.StablePtr +import Simplex.Chat.Controller (ChatController (..)) import Simplex.Chat.Mobile.Shared import qualified Simplex.Messaging.Crypto as C +import UnliftIO (atomically) -cChatEncryptMedia :: CString -> Ptr Word8 -> CInt -> IO CString -cChatEncryptMedia = cTransformMedia chatEncryptMedia +cChatEncryptMedia :: StablePtr ChatController -> CString -> Ptr Word8 -> CInt -> IO CString +cChatEncryptMedia = cTransformMedia . chatEncryptMedia cChatDecryptMedia :: CString -> Ptr Word8 -> CInt -> IO CString cChatDecryptMedia = cTransformMedia chatDecryptMedia @@ -39,11 +43,12 @@ cTransformMedia f cKey cFrame cFrameLen = do putFrame s = when (B.length s <= fromIntegral cFrameLen) $ putByteString cFrame s {-# INLINE cTransformMedia #-} -chatEncryptMedia :: ByteString -> ByteString -> ExceptT String IO ByteString -chatEncryptMedia keyStr frame = do +chatEncryptMedia :: StablePtr ChatController -> ByteString -> ByteString -> ExceptT String IO ByteString +chatEncryptMedia cc keyStr frame = do + ChatController {random} <- liftIO $ deRefStablePtr cc len <- checkFrameLen frame key <- decodeKey keyStr - iv <- liftIO C.randomGCMIV + iv <- atomically $ C.randomGCMIV random (tag, frame') <- withExceptT show $ C.encryptAESNoPad key iv $ B.take len frame pure $ frame' <> BA.convert (C.unAuthTag tag) <> C.unGCMIV iv diff --git a/src/Simplex/Chat/Remote.hs b/src/Simplex/Chat/Remote.hs index 3d98eb7e3..f3d0ba4d1 100644 --- a/src/Simplex/Chat/Remote.hs +++ b/src/Simplex/Chat/Remote.hs @@ -142,7 +142,7 @@ startRemoteHost rh_ rcAddrPrefs_ port_ = do Just (rhId, multicast) -> do rh@RemoteHost {hostPairing} <- withStore $ \db -> getRemoteHost db rhId pure (RHId rhId, multicast, Just $ remoteHostInfo rh $ Just RHSStarting, hostPairing) -- get from the database, start multicast if requested - Nothing -> (RHNew,False,Nothing,) <$> rcNewHostPairing + Nothing -> withAgent $ \a -> (RHNew,False,Nothing,) <$> rcNewHostPairing a sseq <- startRemoteHostSession rhKey ctrlAppInfo <- mkCtrlAppInfo (localAddrs, invitation, rchClient, vars) <- handleConnectError rhKey sseq . withAgent $ \a -> rcConnectHost a pairing (J.toJSON ctrlAppInfo) multicast rcAddrPrefs_ port_ @@ -352,7 +352,7 @@ storeRemoteFile rhId encrypted_ localPath = do tmpDir <- getChatTempDirectory createDirectoryIfMissing True tmpDir tmpFile <- tmpDir `uniqueCombine` takeFileName localPath - cfArgs <- liftIO CF.randomArgs + cfArgs <- atomically . CF.randomArgs =<< asks random liftError (ChatError . CEFileWrite tmpFile) $ encryptFile localPath tmpFile cfArgs pure $ CryptoFile tmpFile $ Just cfArgs diff --git a/src/Simplex/Chat/Remote/Protocol.hs b/src/Simplex/Chat/Remote/Protocol.hs index af4c7d33e..b8ff84709 100644 --- a/src/Simplex/Chat/Remote/Protocol.hs +++ b/src/Simplex/Chat/Remote/Protocol.hs @@ -78,7 +78,7 @@ $(deriveJSON (taggedObjectJSON $ dropPrefix "RR") ''RemoteResponse) mkRemoteHostClient :: ChatMonad m => HTTP2Client -> HostSessKeys -> SessionCode -> FilePath -> HostAppInfo -> m RemoteHostClient mkRemoteHostClient httpClient sessionKeys sessionCode storePath HostAppInfo {encoding, deviceName, encryptFiles} = do - drg <- asks $ agentDRG . smpAgent + drg <- asks random counter <- newTVarIO 1 let HostSessKeys {hybridKey, idPrivKey, sessPrivKey} = sessionKeys signatures = RSSign {idPrivKey, sessPrivKey} @@ -95,7 +95,7 @@ mkRemoteHostClient httpClient sessionKeys sessionCode storePath HostAppInfo {enc mkCtrlRemoteCrypto :: ChatMonad m => CtrlSessKeys -> SessionCode -> m RemoteCrypto mkCtrlRemoteCrypto CtrlSessKeys {hybridKey, idPubKey, sessPubKey} sessionCode = do - drg <- asks $ agentDRG . smpAgent + drg <- asks random counter <- newTVarIO 1 let signatures = RSVerify {idPubKey, sessPubKey} pure RemoteCrypto {drg, counter, sessionCode, hybridKey, signatures} diff --git a/src/Simplex/Chat/Remote/Transport.hs b/src/Simplex/Chat/Remote/Transport.hs index ccd10b328..1c9c3f08e 100644 --- a/src/Simplex/Chat/Remote/Transport.hs +++ b/src/Simplex/Chat/Remote/Transport.hs @@ -24,7 +24,7 @@ type EncryptedFile = ((Handle, Word32), C.CbNonce, LC.SbState) prepareEncryptedFile :: RemoteCrypto -> (Handle, Word32) -> ExceptT RemoteProtocolError IO EncryptedFile prepareEncryptedFile RemoteCrypto {drg, hybridKey} f = do - nonce <- atomically $ C.pseudoRandomCbNonce drg + nonce <- atomically $ C.randomCbNonce drg sbState <- liftEitherWith (const $ PRERemoteControl RCEEncrypt) $ LC.kcbInit hybridKey nonce pure (f, nonce, sbState) diff --git a/src/Simplex/Chat/Store/Shared.hs b/src/Simplex/Chat/Store/Shared.hs index e1125adc3..1e69d7076 100644 --- a/src/Simplex/Chat/Store/Shared.hs +++ b/src/Simplex/Chat/Store/Shared.hs @@ -15,7 +15,7 @@ import qualified Control.Exception as E import Control.Monad import Control.Monad.Except import Control.Monad.IO.Class -import Crypto.Random (ChaChaDRG, randomBytesGenerate) +import Crypto.Random (ChaChaDRG) import qualified Data.Aeson.TH as J import qualified Data.ByteString.Base64 as B64 import Data.ByteString.Char8 (ByteString) @@ -35,6 +35,7 @@ import Simplex.Chat.Types.Preferences import Simplex.Messaging.Agent.Protocol (AgentMsgId, ConnId, UserId) import Simplex.Messaging.Agent.Store.SQLite (firstRow, maybeFirstRow) import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB +import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Parsers (dropPrefix, sumTypeJSON) import Simplex.Messaging.Protocol (SubscriptionMode (..)) import Simplex.Messaging.Util (allFinally) @@ -389,7 +390,4 @@ createWithRandomBytes size gVar create = tryCreate 3 | otherwise -> throwError . SEInternalError $ show e encodedRandomBytes :: TVar ChaChaDRG -> Int -> IO ByteString -encodedRandomBytes gVar = fmap B64.encode . randomBytes gVar - -randomBytes :: TVar ChaChaDRG -> Int -> IO ByteString -randomBytes gVar = atomically . stateTVar gVar . randomBytesGenerate +encodedRandomBytes gVar n = atomically $ B64.encode <$> C.randomBytes n gVar diff --git a/tests/ChatTests/Files.hs b/tests/ChatTests/Files.hs index 4396a900d..2a0736bc6 100644 --- a/tests/ChatTests/Files.hs +++ b/tests/ChatTests/Files.hs @@ -1094,7 +1094,7 @@ testXFTPFileTransferEncrypted = let srcPath = "./tests/tmp/alice/test.pdf" createDirectoryIfMissing True "./tests/tmp/alice/" createDirectoryIfMissing True "./tests/tmp/bob/" - WFResult cfArgs <- chatWriteFile srcPath src + WFResult cfArgs <- chatWriteFile (chatController alice) srcPath src let fileJSON = LB.unpack $ J.encode $ CryptoFile srcPath $ Just cfArgs withXFTPServer $ do connectUsers alice bob diff --git a/tests/MobileTests.hs b/tests/MobileTests.hs index 64fb7c98b..a6231fa27 100644 --- a/tests/MobileTests.hs +++ b/tests/MobileTests.hs @@ -8,8 +8,8 @@ module MobileTests where import ChatTests.Utils +import Control.Concurrent.STM import Control.Monad.Except -import Crypto.Random (getRandomBytes) import Data.Aeson (FromJSON) import qualified Data.Aeson as J import qualified Data.Aeson.TH as JQ @@ -22,8 +22,10 @@ import Data.Word (Word8, Word32) import Foreign.C import Foreign.Marshal.Alloc (mallocBytes) import Foreign.Ptr +import Foreign.StablePtr import Foreign.Storable (peek) import GHC.IO.Encoding (setLocaleEncoding, setFileSystemEncoding, setForeignEncoding) +import Simplex.Chat.Controller (ChatController (..)) import Simplex.Chat.Mobile import Simplex.Chat.Mobile.File import Simplex.Chat.Mobile.Shared @@ -226,25 +228,29 @@ testChatApi tmp = do chatParseMarkdown "*hello*" `shouldBe` parsedMarkdown testMediaApi :: HasCallStack => FilePath -> IO () -testMediaApi _ = do - key :: ByteString <- getRandomBytes 32 - frame <- getRandomBytes 100 +testMediaApi tmp = do + Right c@ChatController {random = g} <- chatMigrateInit (tmp "1") "" "yesUp" + cc <- newStablePtr c + key <- atomically $ C.randomBytes 32 g + frame <- atomically $ C.randomBytes 100 g let keyStr = strEncode key reserved = B.replicate (C.authTagSize + C.gcmIVSize) 0 frame' = frame <> reserved - Right encrypted <- runExceptT $ chatEncryptMedia keyStr frame' + Right encrypted <- runExceptT $ chatEncryptMedia cc keyStr frame' encrypted `shouldNotBe` frame' B.length encrypted `shouldBe` B.length frame' runExceptT (chatDecryptMedia keyStr encrypted) `shouldReturn` Right frame' testMediaCApi :: HasCallStack => FilePath -> IO () -testMediaCApi _ = do - key :: ByteString <- getRandomBytes 32 - frame <- getRandomBytes 100 +testMediaCApi tmp = do + Right c@ChatController {random = g} <- chatMigrateInit (tmp "1") "" "yesUp" + cc <- newStablePtr c + key <- atomically $ C.randomBytes 32 g + frame <- atomically $ C.randomBytes 100 g let keyStr = strEncode key reserved = B.replicate (C.authTagSize + C.gcmIVSize) 0 frame' = frame <> reserved - encrypted <- test cChatEncryptMedia keyStr frame' + encrypted <- test (cChatEncryptMedia cc) keyStr frame' encrypted `shouldNotBe` frame' test cChatDecryptMedia keyStr encrypted `shouldReturn` frame' where @@ -266,6 +272,7 @@ instance FromJSON ReadFileResult where testFileCApi :: FilePath -> FilePath -> IO () testFileCApi fileName tmp = do + cc <- mkCCPtr tmp src <- B.readFile "./tests/fixtures/test.pdf" let path = tmp (fileName <> ".pdf") cPath <- newCString path @@ -273,7 +280,7 @@ testFileCApi fileName tmp = do cLen = fromIntegral len ptr <- mallocBytes $ B.length src putByteString ptr src - r <- peekCAString =<< cChatWriteFile cPath ptr cLen + r <- peekCAString =<< cChatWriteFile cc cPath ptr cLen Just (WFResult cfArgs@(CFArgs key nonce)) <- jDecode r let encryptedFile = CryptoFile path $ Just cfArgs CF.getFileContentsSize encryptedFile `shouldReturn` fromIntegral (B.length src) @@ -292,7 +299,7 @@ testMissingFileCApi :: FilePath -> IO () testMissingFileCApi tmp = do let path = tmp "missing_file" cPath <- newCString path - CFArgs key nonce <- CF.randomArgs + CFArgs key nonce <- atomically . CF.randomArgs =<< C.newRandom cKey <- encodedCString key cNonce <- encodedCString nonce ptr <- cChatReadFile cPath cKey cNonce @@ -302,13 +309,14 @@ testMissingFileCApi tmp = do testFileEncryptionCApi :: FilePath -> FilePath -> IO () testFileEncryptionCApi fileName tmp = do + cc <- mkCCPtr tmp let fromPath = tmp (fileName <> ".source.pdf") copyFile "./tests/fixtures/test.pdf" fromPath src <- B.readFile fromPath cFromPath <- newCString fromPath let toPath = tmp (fileName <> ".encrypted.pdf") cToPath <- newCString toPath - r <- peekCAString =<< cChatEncryptFile cFromPath cToPath + r <- peekCAString =<< cChatEncryptFile cc cFromPath cToPath Just (WFResult cfArgs@(CFArgs key nonce)) <- jDecode r CF.getFileContentsSize (CryptoFile toPath $ Just cfArgs) `shouldReturn` fromIntegral (B.length src) cKey <- encodedCString key @@ -320,14 +328,15 @@ testFileEncryptionCApi fileName tmp = do testMissingFileEncryptionCApi :: FilePath -> IO () testMissingFileEncryptionCApi tmp = do + cc <- mkCCPtr tmp let fromPath = tmp "missing_file.source.pdf" toPath = tmp "missing_file.encrypted.pdf" cFromPath <- newCString fromPath cToPath <- newCString toPath - r <- peekCAString =<< cChatEncryptFile cFromPath cToPath + r <- peekCAString =<< cChatEncryptFile cc cFromPath cToPath Just (WFError err) <- jDecode r err `shouldContain` fromPath - CFArgs key nonce <- CF.randomArgs + CFArgs key nonce <- atomically . CF.randomArgs =<< C.newRandom cKey <- encodedCString key cNonce <- encodedCString nonce let toPath' = tmp "missing_file.decrypted.pdf" @@ -335,6 +344,9 @@ testMissingFileEncryptionCApi tmp = do err' <- peekCAString =<< cChatDecryptFile cToPath cKey cNonce cToPath' err' `shouldContain` toPath +mkCCPtr :: FilePath -> IO (StablePtr ChatController) +mkCCPtr tmp = either (error . show) newStablePtr =<< chatMigrateInit (tmp "1") "" "yesUp" + testValidNameCApi :: FilePath -> IO () testValidNameCApi _ = do let goodName = "Джон Доу 👍" diff --git a/tests/RemoteTests.hs b/tests/RemoteTests.hs index 13bc2942f..ff0e5cb2d 100644 --- a/tests/RemoteTests.hs +++ b/tests/RemoteTests.hs @@ -11,18 +11,14 @@ import Control.Logger.Simple import qualified Data.Aeson as J import qualified Data.ByteString as B import qualified Data.ByteString.Lazy.Char8 as LB -import Data.List.NonEmpty (NonEmpty (..)) import qualified Data.Map.Strict as M -import qualified Network.TLS as TLS import Simplex.Chat.Archive (archiveFilesFolder) import Simplex.Chat.Controller (ChatConfig (..), XFTPFileConfig (..), versionNumber) import qualified Simplex.Chat.Controller as Controller import Simplex.Chat.Mobile.File import Simplex.Chat.Remote.Types -import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Crypto.File (CryptoFileArgs (..)) import Simplex.Messaging.Encoding.String (strEncode) -import Simplex.Messaging.Transport.Credentials (genCredentials, tlsCredentials) import Simplex.Messaging.Util import System.FilePath (()) import Test.Hspec @@ -571,12 +567,6 @@ contactBob desktop bob = do (desktop <## "bob (Bob): contact is connected") (bob <## "alice (Alice): contact is connected") -genTestCredentials :: IO (C.KeyHash, TLS.Credentials) -genTestCredentials = do - caCreds <- liftIO $ genCredentials Nothing (0, 24) "CA" - sessionCreds <- liftIO $ genCredentials (Just caCreds) (0, 24) "Session" - pure . tlsCredentials $ sessionCreds :| [caCreds] - stopDesktop :: HasCallStack => TestCC -> TestCC -> IO () stopDesktop mobile desktop = do logWarn "stopping via desktop" diff --git a/tests/Test.hs b/tests/Test.hs index ee5804aa9..21aa379c1 100644 --- a/tests/Test.hs +++ b/tests/Test.hs @@ -26,7 +26,7 @@ main = do describe "JSON Tests" jsonTests describe "SimpleX chat view" viewTests describe "SimpleX chat protocol" protocolTests - describe "WebRTC encryption" webRTCTests + around tmpBracket $ describe "WebRTC encryption" webRTCTests describe "Valid names" validNameTests around testBracket $ do describe "Mobile API Tests" mobileTests @@ -35,10 +35,11 @@ main = do xdescribe'' "SimpleX Directory service bot" directoryServiceTests describe "Remote session" remoteTests where - testBracket test = do + testBracket test = withSmpServer $ tmpBracket test + tmpBracket test = do t <- getSystemTime let ts = show (systemSeconds t) <> show (systemNanoseconds t) - withSmpServer $ withTmpFiles $ withTempDirectory "tests/tmp" ts test + withTmpFiles $ withTempDirectory "tests/tmp" ts test logCfg :: LogConfig logCfg = LogConfig {lc_file = Nothing, lc_stderr = True} diff --git a/tests/WebRTCTests.hs b/tests/WebRTCTests.hs index 7dd24e608..a473afef3 100644 --- a/tests/WebRTCTests.hs +++ b/tests/WebRTCTests.hs @@ -1,36 +1,49 @@ +{-# LANGUAGE OverloadedStrings #-} + module WebRTCTests where import Control.Monad.Except import Crypto.Random (getRandomBytes) import qualified Data.ByteString.Base64.URL as U import qualified Data.ByteString.Char8 as B +import Foreign.StablePtr +import Simplex.Chat.Mobile import Simplex.Chat.Mobile.WebRTC import qualified Simplex.Messaging.Crypto as C +import System.FilePath (()) import Test.Hspec -webRTCTests :: Spec +webRTCTests :: SpecWith FilePath webRTCTests = describe "WebRTC crypto" $ do - it "encrypts and decrypts media" $ do + it "encrypts and decrypts media" $ \tmp -> do + Right c <- chatMigrateInit (tmp "1") "" "yesUp" + cc <- newStablePtr c key <- U.encode <$> getRandomBytes 32 frame <- getRandomBytes 1000 - Right frame' <- runExceptT $ chatEncryptMedia key $ frame <> B.replicate reservedSize '\NUL' + Right frame' <- runExceptT $ chatEncryptMedia cc key $ frame <> B.replicate reservedSize '\NUL' B.length frame' `shouldBe` B.length frame + reservedSize Right frame'' <- runExceptT $ chatDecryptMedia key frame' frame'' `shouldBe` frame <> B.replicate reservedSize '\NUL' - it "should fail on invalid frame size" $ do + it "should fail on invalid frame size" $ \tmp -> do + Right c <- chatMigrateInit (tmp "1") "" "yesUp" + cc <- newStablePtr c key <- U.encode <$> getRandomBytes 32 frame <- getRandomBytes 10 - runExceptT (chatEncryptMedia key frame) `shouldReturn` Left "frame has no [reserved space for] IV and/or auth tag" + runExceptT (chatEncryptMedia cc key frame) `shouldReturn` Left "frame has no [reserved space for] IV and/or auth tag" runExceptT (chatDecryptMedia key frame) `shouldReturn` Left "frame has no [reserved space for] IV and/or auth tag" - it "should fail on invalid key" $ do + it "should fail on invalid key" $ \tmp -> do + Right c <- chatMigrateInit (tmp "1") "" "yesUp" + cc <- newStablePtr c let key = B.replicate 32 '#' frame <- (<> B.replicate reservedSize '\NUL') <$> getRandomBytes 100 - runExceptT (chatEncryptMedia key frame) `shouldReturn` Left "invalid key: invalid character at offset: 0" + runExceptT (chatEncryptMedia cc key frame) `shouldReturn` Left "invalid key: invalid character at offset: 0" runExceptT (chatDecryptMedia key frame) `shouldReturn` Left "invalid key: invalid character at offset: 0" - it "should fail on invalid auth tag" $ do + it "should fail on invalid auth tag" $ \tmp -> do + Right c <- chatMigrateInit (tmp "1") "" "yesUp" + cc <- newStablePtr c key <- U.encode <$> getRandomBytes 32 frame <- getRandomBytes 1000 - Right frame' <- runExceptT $ chatEncryptMedia key $ frame <> B.replicate reservedSize '\NUL' + Right frame' <- runExceptT $ chatEncryptMedia cc key $ frame <> B.replicate reservedSize '\NUL' Right frame'' <- runExceptT $ chatDecryptMedia key frame' frame'' `shouldBe` frame <> B.replicate reservedSize '\NUL' let (encFrame, rest) = B.splitAt (B.length frame' - reservedSize) frame