From ed3be9c228cd005ee7055c70d20c1b6de0df0ab3 Mon Sep 17 00:00:00 2001 From: sh <37271604+shumvgolove@users.noreply.github.com> Date: Tue, 30 Dec 2025 09:03:49 +0000 Subject: [PATCH 01/73] desktop: add fixed copyright (#6533) * desktop: add fixed copyright Also fixes reproducible builds. * update --------- Co-authored-by: Evgeny --- apps/multiplatform/desktop/build.gradle.kts | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/multiplatform/desktop/build.gradle.kts b/apps/multiplatform/desktop/build.gradle.kts index 60ff535e88..1e7bda37c4 100644 --- a/apps/multiplatform/desktop/build.gradle.kts +++ b/apps/multiplatform/desktop/build.gradle.kts @@ -40,6 +40,7 @@ compose { } mainClass = "chat.simplex.desktop.MainKt" nativeDistributions { + copyright = "(c) 2020-2026 SimpleX Chat" // For debugging via VisualVM if (debugJava) { modules("jdk.zipfs", "jdk.unsupported", "jdk.management.agent") From f0467aee0010f4e2ccd81b0c540e5e324edb1ceb Mon Sep 17 00:00:00 2001 From: Evgeny Date: Sun, 4 Jan 2026 19:04:32 +0000 Subject: [PATCH 02/73] directory service: fix queries (#6539) * fix directory service queries * fix * reduce postgres pool size to 1 * stabilize postgres client tests, remove slow handshake tests * update simplexmq * fix test * test delay --- .../src/Directory/Store.hs | 12 +-- cabal.project | 2 +- scripts/nix/sha256map.nix | 2 +- src/Simplex/Chat/Controller.hs | 32 +++--- src/Simplex/Chat/Library/Commands.hs | 4 +- src/Simplex/Chat/Options/Postgres.hs | 4 +- tests/ChatClient.hs | 20 ++-- tests/ChatTests/Direct.hs | 99 ++++++------------- tests/ChatTests/Files.hs | 4 +- tests/ChatTests/Groups.hs | 98 +++++------------- tests/ChatTests/Profiles.hs | 97 +++++------------- 11 files changed, 117 insertions(+), 257 deletions(-) diff --git a/apps/simplex-directory-service/src/Directory/Store.hs b/apps/simplex-directory-service/src/Directory/Store.hs index b78b446821..b5f7220724 100644 --- a/apps/simplex-directory-service/src/Directory/Store.hs +++ b/apps/simplex-directory-service/src/Directory/Store.hs @@ -351,11 +351,11 @@ searchListedGroups cc user@User {userId, userContactId} searchType lastGroup_ pa pure (gs, n) Just gId -> do gs <- groups $ DB.query db (listedGroupQuery <> " AND r.group_id > ? " <> orderBy <> " LIMIT ?") (userId, userContactId, GRSActive, gId, pageSize) - n <- count $ DB.query db (countQuery' <> " AND r.group_id > ? " <> orderBy) (GRSActive, gId) + n <- count $ DB.query db (countQuery' <> " AND r.group_id > ?") (GRSActive, gId) pure (gs, n) where countQuery' = countQuery <> " WHERE r.group_reg_status = ? " - orderBy = " ORDER BY g.summary_current_members_count DESC " + orderBy = " ORDER BY g.summary_current_members_count DESC, r.group_reg_id ASC " STRecent -> case lastGroup_ of Nothing -> do gs <- groups $ DB.query db (listedGroupQuery <> orderBy <> " LIMIT ?") (userId, userContactId, GRSActive, pageSize) @@ -363,11 +363,11 @@ searchListedGroups cc user@User {userId, userContactId} searchType lastGroup_ pa pure (gs, n) Just gId -> do gs <- groups $ DB.query db (listedGroupQuery <> " AND r.group_id > ? " <> orderBy <> " LIMIT ?") (userId, userContactId, GRSActive, gId, pageSize) - n <- count $ DB.query db (countQuery' <> " AND r.group_id > ? " <> orderBy) (GRSActive, gId) + n <- count $ DB.query db (countQuery' <> " AND r.group_id > ?") (GRSActive, gId) pure (gs, n) where countQuery' = countQuery <> " WHERE r.group_reg_status = ? " - orderBy = " ORDER BY r.created_at DESC " + orderBy = " ORDER BY r.created_at DESC, r.group_reg_id ASC " STSearch search -> case lastGroup_ of Nothing -> do gs <- groups $ DB.query db (listedGroupQuery <> searchCond <> orderBy <> " LIMIT ?") (userId, userContactId, GRSActive, s, s, s, s, pageSize) @@ -375,12 +375,12 @@ searchListedGroups cc user@User {userId, userContactId} searchType lastGroup_ pa pure (gs, n) Just gId -> do gs <- groups $ DB.query db (listedGroupQuery <> " AND r.group_id > ? " <> searchCond <> orderBy <> " LIMIT ?") (userId, userContactId, GRSActive, gId, s, s, s, s, pageSize) - n <- count $ DB.query db (countQuery' <> " AND r.group_id > ? " <> searchCond <> orderBy) (GRSActive, gId, s, s, s, s) + n <- count $ DB.query db (countQuery' <> " AND r.group_id > ? " <> searchCond) (GRSActive, gId, s, s, s, s) pure (gs, n) where s = T.toLower search countQuery' = countQuery <> " JOIN group_profiles gp ON gp.group_profile_id = g.group_profile_id WHERE r.group_reg_status = ? " - orderBy = " ORDER BY g.summary_current_members_count DESC " + orderBy = " ORDER BY g.summary_current_members_count DESC, r.group_reg_id ASC " where groups = (map (toGroupInfoReg (vr cc) user) <$>) count = maybeFirstRow' 0 fromOnly diff --git a/cabal.project b/cabal.project index e9842b1138..e5d7464ece 100644 --- a/cabal.project +++ b/cabal.project @@ -12,7 +12,7 @@ constraints: zip +disable-bzip2 +disable-zstd source-repository-package type: git location: https://github.com/simplex-chat/simplexmq.git - tag: 5f73d1e629a8807f1b9d94f8b411d6480a0a59fb + tag: a7b43b1a3e204759d4b7ad60928fa897b1600654 source-repository-package type: git diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index 3cfc2a05af..16454f63d1 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,5 +1,5 @@ { - "https://github.com/simplex-chat/simplexmq.git"."5f73d1e629a8807f1b9d94f8b411d6480a0a59fb" = "1w5mxw9rwiiiqphbg2rdyp4cvv9hz2l64f7fpfhncw6gncfx7ggw"; + "https://github.com/simplex-chat/simplexmq.git"."a7b43b1a3e204759d4b7ad60928fa897b1600654" = "169vjn5gyw42cmak6kwyl27zm57il43khnlj40zjwjw7cldkzdzi"; "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/Controller.hs b/src/Simplex/Chat/Controller.hs index 34ad95b800..5754419933 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -96,7 +96,12 @@ import Simplex.RemoteControl.Types import System.IO (Handle) import System.Mem.Weak (Weak) import UnliftIO.STM -#if !defined(dbPostgres) + +#if defined(dbPostgres) +import qualified Database.PostgreSQL.Simple as PSQL + +type SQLError = PSQL.SqlError +#else import Database.SQLite.Simple (SQLError) import qualified Database.SQLite.Simple as SQL import Simplex.Messaging.Agent.Store.SQLite.DB (SlowQueryStats (..)) @@ -1542,25 +1547,24 @@ withFastStore = withStorePriority True withStorePriority :: Bool -> (DB.Connection -> ExceptT StoreError IO a) -> CM a withStorePriority priority action = do ChatController {chatStore} <- ask - liftIOEither $ withTransactionPriority chatStore priority (runExceptT . withExceptT ChatErrorStore . action) `E.catches` handleDBErrors + liftIOEither $ withTransactionPriority chatStore priority (runExceptT . withExceptT ChatErrorStore . action) `E.catch` handleDBErrors withStoreBatch :: Traversable t => (DB.Connection -> t (IO (Either ChatError a))) -> CM' (t (Either ChatError a)) withStoreBatch actions = do ChatController {chatStore} <- ask - liftIO $ withTransaction chatStore $ mapM (`E.catches` handleDBErrors) . actions + liftIO $ withTransaction chatStore $ mapM (`E.catch` handleDBErrors) . actions --- TODO [postgres] postgres specific error handling -handleDBErrors :: [E.Handler (Either ChatError a)] -handleDBErrors = -#if !defined(dbPostgres) - ( E.Handler $ \(e :: SQLError) -> - let se = SQL.sqlError e - busy = se == SQL.ErrorBusy || se == SQL.ErrorLocked - in pure . Left . ChatErrorStore $ if busy then SEDBBusyError $ show se else SEDBException $ show e - ) : +handleDBErrors :: E.SomeException -> IO (Either ChatError a) +handleDBErrors e = pure $ Left $ ChatErrorStore $ case E.fromException e of + Just (e' :: SQLError) -> +#if defined(dbPostgres) + SEDBException $ show e' +#else + let se = SQL.sqlError e' + busy = se == SQL.ErrorBusy || se == SQL.ErrorLocked + in (if busy then SEDBBusyError else SEDBException) $ show e' #endif - [ E.Handler $ \(E.SomeException e) -> pure . Left . ChatErrorStore . SEDBException $ show e - ] + Nothing -> SEDBException $ show e withStoreBatch' :: Traversable t => (DB.Connection -> t (IO a)) -> CM' (t (Either ChatError a)) withStoreBatch' actions = withStoreBatch $ fmap (fmap Right) . actions diff --git a/src/Simplex/Chat/Library/Commands.hs b/src/Simplex/Chat/Library/Commands.hs index fc6a0ea782..675ca03a8a 100644 --- a/src/Simplex/Chat/Library/Commands.hs +++ b/src/Simplex/Chat/Library/Commands.hs @@ -262,7 +262,7 @@ stopChatController ChatController {smpAgent, agentAsync = s, sndFiles, rcvFiles, readTVarIO remoteHostSessions >>= mapM_ (cancelRemoteHost False . snd) atomically (stateTVar remoteCtrlSession (,Nothing)) >>= mapM_ (cancelRemoteCtrl False . snd) disconnectAgentClient smpAgent - readTVarIO s >>= mapM_ (\(a1, a2) -> uninterruptibleCancel a1 >> mapM_ uninterruptibleCancel a2) + readTVarIO s >>= mapM_ (\(a1, a2) -> forkIO $ uninterruptibleCancel a1 >> mapM_ uninterruptibleCancel a2) closeFiles sndFiles closeFiles rcvFiles atomically $ do @@ -1805,7 +1805,7 @@ processChatCommand vr nm = \case conn <- withFastStore $ \db -> getPendingContactConnection db userId connId let PendingContactConnection {pccConnStatus, connLinkInv} = conn case (pccConnStatus, connLinkInv) of - (ConnNew, Just _ссLink) -> do + (ConnNew, Just _ccLink) -> do newUser <- privateGetUser newUserId conn' <- recreateConn user conn newUser pure $ CRConnectionUserChanged user conn conn' newUser diff --git a/src/Simplex/Chat/Options/Postgres.hs b/src/Simplex/Chat/Options/Postgres.hs index c74ae37750..ab7414566c 100644 --- a/src/Simplex/Chat/Options/Postgres.hs +++ b/src/Simplex/Chat/Options/Postgres.hs @@ -42,7 +42,7 @@ chatDbOptsP _appDir defaultDbName = do ( long "pool-size" <> metavar "DB_POOL_SIZE" <> help "Database connection pool size" - <> value 10 + <> value 1 <> showDefault ) dbCreateSchema <- @@ -84,7 +84,7 @@ mobileDbOpts schemaPrefix connstr = do ChatDbOpts { dbConnstr, dbSchemaPrefix, - dbPoolSize = 10, + dbPoolSize = 1, dbCreateSchema = True } diff --git a/tests/ChatClient.hs b/tests/ChatClient.hs index d0b186dd94..e258f3dccc 100644 --- a/tests/ChatClient.hs +++ b/tests/ChatClient.hs @@ -131,7 +131,7 @@ testCoreOpts = -- dbSchemaPrefix is not used in tests (except bot tests where it's redefined), -- instead different schema prefix is passed per client so that single test database is used dbSchemaPrefix = "", - dbPoolSize = 3, + dbPoolSize = 1, dbCreateSchema = True #else { dbFilePrefix = "./simplex_v1", -- dbFilePrefix is not used in tests (except bot tests where it's redefined) @@ -184,16 +184,11 @@ aCfg = (agentConfig defaultChatConfig) {tbqSize = 16} testAgentCfg :: AgentConfig testAgentCfg = aCfg - { reconnectInterval = (reconnectInterval aCfg) {initialInterval = 50000} - } - -testAgentCfgSlow :: AgentConfig -testAgentCfgSlow = - testAgentCfg - { smpClientVRange = mkVersionRange (Version 1) srvHostnamesSMPClientVersion, -- v2 - smpAgentVRange = mkVersionRange duplexHandshakeSMPAgentVersion pqdrSMPAgentVersion, -- v5 - smpCfg = (smpCfg testAgentCfg) {serverVRange = mkVersionRange minClientSMPRelayVersion sendingProxySMPVersion} -- v8 + { reconnectInterval = (reconnectInterval aCfg) {initialInterval = 50000}, + messageRetryInterval = RetryInterval2 {riFast = riFast {initialInterval = 50000}, riSlow = riSlow {initialInterval = 50000}} } + where + RetryInterval2 {riFast, riSlow} = messageRetryInterval aCfg testAgentCfgNoShortLinks :: AgentConfig testAgentCfgNoShortLinks = @@ -213,9 +208,6 @@ testCfg = confirmMigrations = MCYesUp } -testCfgSlow :: ChatConfig -testCfgSlow = testCfg {agentConfig = testAgentCfgSlow} - testCfgNoShortLinks :: ChatConfig testCfgNoShortLinks = testCfg {agentConfig = testAgentCfgNoShortLinks} @@ -522,7 +514,7 @@ smpServerCfg :: ServerConfig STMMsgStore smpServerCfg = ServerConfig { transports = [(serverPort, transport @TLS, False)], - tbqSize = 1, + tbqSize = 4, msgQueueQuota = 16, maxJournalMsgCount = 24, maxJournalStateLines = 4, diff --git a/tests/ChatTests/Direct.hs b/tests/ChatTests/Direct.hs index 1b93013258..a56ad6d4e3 100644 --- a/tests/ChatTests/Direct.hs +++ b/tests/ChatTests/Direct.hs @@ -94,22 +94,9 @@ chatDirectTests = do describe "operators and usage conditions" $ do it "get and enable operators, accept conditions" testOperators describe "async connection handshake" $ do - describe "connect when initiating client goes offline" $ do - it "curr" $ testAsyncInitiatingOffline True testCfg testCfg - it "v5" $ testAsyncInitiatingOffline False testCfgSlow testCfgSlow - it "v5/curr" $ testAsyncInitiatingOffline False testCfgSlow testCfg - it "curr/v5" $ testAsyncInitiatingOffline True testCfg testCfgSlow - describe "connect when accepting client goes offline" $ do - it "curr" $ testAsyncAcceptingOffline True testCfg testCfg - it "v5" $ testAsyncAcceptingOffline False testCfgSlow testCfgSlow - it "v5/curr" $ testAsyncAcceptingOffline False testCfgSlow testCfg - it "curr/v5" $ testAsyncAcceptingOffline True testCfg testCfgSlow - describe "connect, fully asynchronous (when clients are never simultaneously online)" $ do - it "curr" testFullAsyncFast - -- fails in CI - xit'' "v5" $ testFullAsyncSlow False testCfgSlow testCfgSlow - xit'' "v5/curr" $ testFullAsyncSlow False testCfgSlow testCfg - xit'' "curr/v5" $ testFullAsyncSlow True testCfg testCfgSlow + it "connect when initiating client goes offline" $ testAsyncInitiatingOffline True + it "connect when accepting client goes offline" $ testAsyncAcceptingOffline True + it "connect, fully asynchronous (when clients are never simultaneously online)" $ testFullAsyncFast describe "webrtc calls api" $ do it "negotiate call" testNegotiateCall #if !defined(dbPostgres) @@ -1241,33 +1228,33 @@ testOperators = where opts' = testOpts {coreOptions = testCoreOpts {smpServers = [], xftpServers = []}} -testAsyncInitiatingOffline :: HasCallStack => Bool -> ChatConfig -> ChatConfig -> TestParams -> IO () -testAsyncInitiatingOffline withShortLink aliceCfg bobCfg ps = do - inv <- withNewTestChatCfg ps aliceCfg "alice" aliceProfile $ \alice -> do +testAsyncInitiatingOffline :: HasCallStack => Bool -> TestParams -> IO () +testAsyncInitiatingOffline withShortLink ps = do + inv <- withNewTestChat ps "alice" aliceProfile $ \alice -> do threadDelay 250000 alice ##> "/c" (if withShortLink then getInvitation else getInvitationNoShortLink) alice - withNewTestChatCfg ps bobCfg "bob" bobProfile $ \bob -> do + withNewTestChat ps "bob" bobProfile $ \bob -> do threadDelay 250000 bob ##> ("/c " <> inv) bob <## "confirmation sent!" - withTestChatCfg ps aliceCfg "alice" $ \alice -> do + withTestChat ps "alice" $ \alice -> do alice <## "subscribed 1 connections on server localhost" concurrently_ (bob <## "alice (Alice): contact is connected") (alice <## "bob (Bob): contact is connected") -testAsyncAcceptingOffline :: HasCallStack => Bool -> ChatConfig -> ChatConfig -> TestParams -> IO () -testAsyncAcceptingOffline withShortLink aliceCfg bobCfg ps = do - inv <- withNewTestChatCfg ps aliceCfg "alice" aliceProfile $ \alice -> do +testAsyncAcceptingOffline :: HasCallStack => Bool -> TestParams -> IO () +testAsyncAcceptingOffline withShortLink ps = do + inv <- withNewTestChat ps "alice" aliceProfile $ \alice -> do alice ##> "/c" (if withShortLink then getInvitation else getInvitationNoShortLink) alice - withNewTestChatCfg ps bobCfg "bob" bobProfile $ \bob -> do + withNewTestChat ps "bob" bobProfile $ \bob -> do threadDelay 250000 bob ##> ("/c " <> inv) bob <## "confirmation sent!" - withTestChatCfg ps aliceCfg "alice" $ \alice -> do - withTestChatCfg ps bobCfg "bob" $ \bob -> do + withTestChat ps "alice" $ \alice -> do + withTestChat ps "bob" $ \bob -> do alice <## "subscribed 1 connections on server localhost" bob <## "subscribed 1 connections on server localhost" concurrently_ @@ -1292,30 +1279,6 @@ testFullAsyncFast ps = do bob <## "subscribed 1 connections on server localhost" bob <## "alice (Alice): contact is connected" -testFullAsyncSlow :: HasCallStack => Bool -> ChatConfig -> ChatConfig -> TestParams -> IO () -testFullAsyncSlow withShortLink aliceCfg bobCfg ps = do - inv <- withNewTestChatCfg ps aliceCfg "alice" aliceProfile $ \alice -> do - threadDelay 250000 - alice ##> "/c" - (if withShortLink then getInvitation else getInvitationNoShortLink) alice - withNewTestChatCfg ps bobCfg "bob" bobProfile $ \bob -> do - threadDelay 250000 - bob ##> ("/c " <> inv) - bob <## "confirmation sent!" - withAlice $ \alice -> - alice <## "subscribed 1 connections on server localhost" - withBob $ \bob -> - bob <## "subscribed 1 connections on server localhost" - withAlice $ \alice -> do - alice <## "subscribed 1 connections on server localhost" - alice <## "bob (Bob): contact is connected" - withBob $ \bob -> do - bob <## "subscribed 1 connections on server localhost" - bob <## "alice (Alice): contact is connected" - where - withAlice = withTestChatCfg ps aliceCfg "alice" - withBob = withTestChatCfg ps aliceCfg "bob" - testCallType :: CallType testCallType = CallType {media = CMVideo, capabilities = CallCapabilities {encryption = True}} @@ -1341,7 +1304,7 @@ repeatM_ n a = forM_ [1 .. n] $ const a testNegotiateCall :: HasCallStack => TestParams -> IO () testNegotiateCall = - testChat2 aliceProfile bobProfile $ \alice bob -> do + withTestOutput $ testChat2 aliceProfile bobProfile $ \alice bob -> do connectUsers alice bob -- just for testing db query alice ##> "/_call get" @@ -2200,7 +2163,7 @@ testUsersDifferentCIExpirationTTL ps = do showActiveUser alice "alisa" alice #$> ("/_get chat @6 count=100", chat, chatFeatures <> [(1, "alisa 1"), (0, "alisa 2"), (1, "alisa 3"), (0, "alisa 4")]) - threadDelay 2000000 + threadDelay 2100000 alice #$> ("/_get chat @6 count=100", chat, [(1,"chat banner")]) where @@ -2419,7 +2382,7 @@ testDisableCIExpirationOnlyForOneUser ps = do cfg = testCfg {initialCleanupManagerDelay = 0, cleanupManagerStepDelay = 0, ciExpirationInterval = 500000} testUsersTimedMessages :: HasCallStack => TestParams -> IO () -testUsersTimedMessages ps = do +testUsersTimedMessages ps' = do withNewTestChat ps "bob" bobProfile $ \bob -> do withNewTestChat ps "alice" aliceProfile $ \alice -> do connectUsers alice bob @@ -2462,10 +2425,8 @@ testUsersTimedMessages ps = do threadDelay 1000000 - alice <## "[user: alice] timed message deleted: alice 1" - alice <## "[user: alice] timed message deleted: alice 2" - bob <## "timed message deleted: alice 1" - bob <## "timed message deleted: alice 2" + alice <### ["[user: alice] timed message deleted: alice 1", "[user: alice] timed message deleted: alice 2"] + bob <### ["timed message deleted: alice 1", "timed message deleted: alice 2"] alice ##> "/user alice" showActiveUser alice "alice (Alice)" @@ -2477,10 +2438,8 @@ testUsersTimedMessages ps = do threadDelay 1000000 - alice <## "timed message deleted: alisa 1" - alice <## "timed message deleted: alisa 2" - bob <## "timed message deleted: alisa 1" - bob <## "timed message deleted: alisa 2" + alice <### ["timed message deleted: alisa 1", "timed message deleted: alisa 2"] + bob <### ["timed message deleted: alisa 1", "timed message deleted: alisa 2"] alice ##> "/user" showActiveUser alice "alisa" @@ -2519,10 +2478,8 @@ testUsersTimedMessages ps = do -- messages are deleted after restart threadDelay 1000000 - alice <## "[user: alice] timed message deleted: alice 3" - alice <## "[user: alice] timed message deleted: alice 4" - bob <## "timed message deleted: alice 3" - bob <## "timed message deleted: alice 4" + alice <### ["[user: alice] timed message deleted: alice 3", "[user: alice] timed message deleted: alice 4"] + bob <### ["timed message deleted: alice 3", "timed message deleted: alice 4"] alice ##> "/user alice" showActiveUser alice "alice (Alice)" @@ -2534,15 +2491,14 @@ testUsersTimedMessages ps = do threadDelay 1000000 - alice <## "timed message deleted: alisa 3" - alice <## "timed message deleted: alisa 4" - bob <## "timed message deleted: alisa 3" - bob <## "timed message deleted: alisa 4" + alice <### ["timed message deleted: alisa 3", "timed message deleted: alisa 4"] + bob <### ["timed message deleted: alisa 3", "timed message deleted: alisa 4"] alice ##> "/user" showActiveUser alice "alisa" alice #$> ("/_get chat @6 count=100", chat, [(1,"chat banner")]) where + ps = ps' {printOutput = True} :: TestParams configureTimedMessages :: HasCallStack => TestCC -> TestCC -> String -> String -> IO () configureTimedMessages alice bob bobId ttl = do aliceName <- userName alice @@ -2699,7 +2655,7 @@ testUserPrivacy = testSetChatItemTTL :: HasCallStack => TestParams -> IO () testSetChatItemTTL = testChat2 aliceProfile bobProfile $ - \alice bob -> do + \alice bob -> withXFTPServer $ do connectUsers alice bob alice #> "@bob 1" bob <# "alice> 1" @@ -2713,6 +2669,7 @@ testSetChatItemTTL = alice <## "use /fc 1 to cancel sending" bob <# "alice> sends file test.jpg (136.5 KiB / 139737 bytes)" bob <## "use /fr 1 [/ | ] to receive it" + alice <## "completed uploading file 1 (test.jpg) for bob" -- above items should be deleted after we set ttl threadDelay 3000000 alice #> "@bob 3" diff --git a/tests/ChatTests/Files.hs b/tests/ChatTests/Files.hs index 2de9d7ffe5..a71e7ae173 100644 --- a/tests/ChatTests/Files.hs +++ b/tests/ChatTests/Files.hs @@ -761,7 +761,9 @@ testXFTPDeleteUploadedFileGroup = alice ##> "/fc 1" concurrentlyN_ - [ alice <## "cancelled sending file 1 (test.pdf) to bob, cath", + [ do + recipients <- dropStrPrefix "cancelled sending file 1 (test.pdf) to " <$> getTermLine alice + recipients == "bob, cath" || recipients == "cath, bob" `shouldBe` True, cath <## "alice cancelled sending file 1 (test.pdf)" ] diff --git a/tests/ChatTests/Groups.hs b/tests/ChatTests/Groups.hs index aa9a48f279..166528e69c 100644 --- a/tests/ChatTests/Groups.hs +++ b/tests/ChatTests/Groups.hs @@ -94,10 +94,7 @@ chatGroupTests = do describe "batch send messages" $ do it "send multiple messages api" testSendMulti it "send multiple timed messages" testSendMultiTimed -#if !defined(dbPostgres) - -- TODO [postgres] this test hangs with PostgreSQL it "send multiple messages (many chat batches)" testSendMultiManyBatches -#endif it "shared message body is reused" testSharedMessageBody it "shared batch body is reused" testSharedBatchBody describe "async group connections" $ do @@ -124,7 +121,6 @@ chatGroupTests = do it "ok to connect; known group" testPlanGroupLinkKnown it "own group link" testPlanGroupLinkOwn it "group link without contact - connecting" testPlanGroupLinkConnecting - it "group link without contact - connecting (slow handshake)" testPlanGroupLinkConnectingSlow it "re-join existing group after leaving" testPlanGroupLinkLeaveRejoin #if !defined(dbPostgres) -- TODO [postgres] restore from outdated db backup (same as in agent) @@ -2044,20 +2040,17 @@ testSendMultiManyBatches = (bob <# ("#team alice> message " <> show i)) (cath <# ("#team alice> message " <> show i)) - aliceItemsCount <- withCCTransaction alice $ \db -> - DB.query db "SELECT count(1) FROM chat_items WHERE chat_item_id > ?" (Only msgIdAlice) :: IO [[Int]] - aliceItemsCount `shouldBe` [[300]] - - bobItemsCount <- withCCTransaction bob $ \db -> - DB.query db "SELECT count(1) FROM chat_items WHERE chat_item_id > ?" (Only msgIdBob) :: IO [[Int]] - bobItemsCount `shouldBe` [[300]] - - cathItemsCount <- withCCTransaction cath $ \db -> - DB.query db "SELECT count(1) FROM chat_items WHERE chat_item_id > ?" (Only msgIdCath) :: IO [[Int]] - cathItemsCount `shouldBe` [[300]] + checkItemCount alice msgIdAlice 300 + checkItemCount bob msgIdBob 300 + checkItemCount cath msgIdCath 300 + where + checkItemCount c msgId n = do + itemsCount <- withCCTransaction c $ \db -> + DB.query db "SELECT count(1) FROM chat_items WHERE chat_item_id > ?" (Only msgId) :: IO [[Int]] + itemsCount `shouldBe` [[n]] testSharedMessageBody :: HasCallStack => TestParams -> IO () -testSharedMessageBody ps = +testSharedMessageBody ps' = withNewTestChatOpts ps opts' "alice" aliceProfile $ \alice -> do withSmpServer' serverCfg' $ withNewTestChatOpts ps opts' "bob" bobProfile $ \bob -> @@ -2066,9 +2059,7 @@ testSharedMessageBody ps = alice <## "disconnected 4 connections on server localhost" alice #> "#team hello" - bodiesCount1 <- withCCAgentTransaction alice $ \db -> - DB.query_ db "SELECT count(1) FROM snd_message_bodies" :: IO [[Int]] - bodiesCount1 `shouldBe` [[1]] + checkMsgBodyCount alice 1 withSmpServer' serverCfg' $ withTestChatOpts ps opts' "bob" $ \bob -> @@ -2080,12 +2071,15 @@ testSharedMessageBody ps = ] bob <# "#team alice> hello" cath <# "#team alice> hello" - bodiesCount2 <- withCCAgentTransaction alice $ \db -> - DB.query_ db "SELECT count(1) FROM snd_message_bodies" :: IO [[Int]] - bodiesCount2 `shouldBe` [[0]] +-- because of PostgreSQL concurrency deleteSndMsgDelivery fails to delete message body +#if !defined(dbPostgres) + threadDelay 500000 + checkMsgBodyCount alice 0 +#endif alice <## "disconnected 4 connections on server localhost" where + ps = ps' {printOutput = True} :: TestParams tmp = tmpPath ps serverCfg' = smpServerCfg @@ -2100,6 +2094,12 @@ testSharedMessageBody ps = } } +checkMsgBodyCount :: TestCC -> Int -> IO () +checkMsgBodyCount c n = do + bodiesCount <- withCCAgentTransaction c $ \db -> + DB.query_ db "SELECT count(1) FROM snd_message_bodies" + bodiesCount `shouldBe` [[n]] + testSharedBatchBody :: HasCallStack => TestParams -> IO () testSharedBatchBody ps = withNewTestChatOpts ps opts' "alice" aliceProfile $ \alice -> do @@ -2116,9 +2116,7 @@ testSharedBatchBody ps = _ <- getTermLine alice alice <## "300 messages sent" - bodiesCount1 <- withCCAgentTransaction alice $ \db -> - DB.query_ db "SELECT count(1) FROM snd_message_bodies" :: IO [[Int]] - bodiesCount1 `shouldBe` [[3]] + checkMsgBodyCount alice 3 withSmpServer' serverCfg' $ withTestChatOpts ps opts' "bob" $ \bob -> @@ -2132,9 +2130,10 @@ testSharedBatchBody ps = concurrently_ (bob <# ("#team alice> message " <> show i)) (cath <# ("#team alice> message " <> show i)) - bodiesCount2 <- withCCAgentTransaction alice $ \db -> - DB.query_ db "SELECT count(1) FROM snd_message_bodies" :: IO [[Int]] - bodiesCount2 `shouldBe` [[0]] +-- because of PostgreSQL concurrency deleteSndMsgDelivery fails to delete message body +#if !defined(dbPostgres) + checkMsgBodyCount alice 0 +#endif alice <## "disconnected 4 connections on server localhost" where @@ -3611,49 +3610,6 @@ testPlanGroupLinkConnecting ps = do bob <## "group link: known group #team" bob <## "use #team to send messages" -testPlanGroupLinkConnectingSlow :: HasCallStack => TestParams -> IO () -testPlanGroupLinkConnectingSlow ps = do - gLink <- withNewTestChatCfg ps testCfgSlow "alice" aliceProfile $ \alice -> do - threadDelay 100000 - alice ##> "/g team" - alice <## "group #team is created" - alice <## "to add members use /a team or /create link #team" - alice ##> "/create link #team" - getGroupLinkNoShortLink alice "team" GRMember True - withNewTestChatCfg ps testCfgSlow "bob" bobProfile $ \bob -> do - threadDelay 100000 - - bob ##> ("/c " <> gLink) - bob <## "connection request sent!" - - bob ##> ("/_connect plan 1 " <> gLink) - bob <## "group link: connecting, allowed to reconnect" - - let gLinkSchema2 = linkAnotherSchema gLink - bob ##> ("/_connect plan 1 " <> gLinkSchema2) - bob <## "group link: connecting, allowed to reconnect" - - threadDelay 100000 - withTestChatCfg ps testCfgSlow "alice" $ \alice -> do - alice - <### [ "subscribed 1 connections on server localhost", - "bob (Bob): accepting request to join group #team..." - ] - withTestChatCfg ps testCfgSlow "bob" $ \bob -> do - threadDelay 500000 - bob <## "subscribed 1 connections on server localhost" - bob <## "#team: joining the group..." - - bob ##> ("/_connect plan 1 " <> gLink) - bob <## "group link: connecting to group #team" - - let gLinkSchema2 = linkAnotherSchema gLink - bob ##> ("/_connect plan 1 " <> gLinkSchema2) - bob <## "group link: connecting to group #team" - - bob ##> ("/c " <> gLink) - bob <## "group link: connecting to group #team" - #if !defined(dbPostgres) testGroupMsgDecryptError :: HasCallStack => TestParams -> IO () testGroupMsgDecryptError ps = diff --git a/tests/ChatTests/Profiles.hs b/tests/ChatTests/Profiles.hs index a1ab8548ed..3fdadc3b64 100644 --- a/tests/ChatTests/Profiles.hs +++ b/tests/ChatTests/Profiles.hs @@ -61,7 +61,6 @@ chatProfileTests = do it "contact address ok to connect; known contact" testPlanAddressOkKnown it "own contact address" testPlanAddressOwn it "connecting via contact address" testPlanAddressConnecting - it "connecting via contact address (slow handshake)" testPlanAddressConnectingSlow it "re-connect with deleted contact" testPlanAddressContactDeletedReconnected it "contact via address" testPlanAddressContactViaAddress it "contact via short address" testPlanAddressContactViaShortAddress @@ -72,7 +71,6 @@ chatProfileTests = do it "set connection incognito" testSetConnectionIncognito it "reset connection incognito" testResetConnectionIncognito it "set connection incognito prohibited during negotiation" testSetConnectionIncognitoProhibitedDuringNegotiation - it "set connection incognito prohibited during negotiation (slow handshake)" testSetConnectionIncognitoProhibitedDuringNegotiationSlow it "connection incognito unchanged errors" testConnectionIncognitoUnchangedErrors it "set, reset, set connection incognito" testSetResetSetConnectionIncognito it "join group incognito" testJoinGroupIncognito @@ -1110,46 +1108,6 @@ testPlanAddressConnecting ps = do bob <## "contact address: known contact alice" bob <## "use @alice to send messages" -testPlanAddressConnectingSlow :: HasCallStack => TestParams -> IO () -testPlanAddressConnectingSlow ps = do - cLink <- withNewTestChatCfg ps testCfgSlow "alice" aliceProfile $ \alice -> do - alice ##> "/ad" - getContactLinkNoShortLink alice True - withNewTestChatCfg ps testCfgSlow "bob" bobProfile $ \bob -> do - threadDelay 100000 - - bob ##> ("/c " <> cLink) - bob <## "connection request sent!" - - bob ##> ("/_connect plan 1 " <> cLink) - bob <## "contact address: connecting, allowed to reconnect" - - let cLinkSchema2 = linkAnotherSchema cLink - bob ##> ("/_connect plan 1 " <> cLinkSchema2) - bob <## "contact address: connecting, allowed to reconnect" - - threadDelay 100000 - withTestChatCfg ps testCfgSlow "alice" $ \alice -> do - alice <## "subscribed 1 connections on server localhost" - alice <## "bob (Bob) wants to connect to you!" - alice <## "to accept: /ac bob" - alice <## "to reject: /rc bob (the sender will NOT be notified)" - alice ##> "/ac bob" - alice <## "bob (Bob): accepting contact request..." - withTestChatCfg ps testCfgSlow "bob" $ \bob -> do - threadDelay 500000 - bob <## "subscribed 1 connections on server localhost" - bob @@@ [("@alice", "")] - bob ##> ("/_connect plan 1 " <> cLink) - bob <## "contact address: connecting to contact alice" - - let cLinkSchema2 = linkAnotherSchema cLink - bob ##> ("/_connect plan 1 " <> cLinkSchema2) - bob <## "contact address: connecting to contact alice" - - bob ##> ("/c " <> cLink) - bob <## "contact address: connecting to contact alice" - testPlanAddressContactDeletedReconnected :: HasCallStack => TestParams -> IO () testPlanAddressContactDeletedReconnected = testChat2 aliceProfile bobProfile $ @@ -1559,30 +1517,6 @@ testSetConnectionIncognitoProhibitedDuringNegotiation ps = do alice `hasContactProfiles` ["alice", "bob"] bob `hasContactProfiles` ["alice", "bob"] -testSetConnectionIncognitoProhibitedDuringNegotiationSlow :: HasCallStack => TestParams -> IO () -testSetConnectionIncognitoProhibitedDuringNegotiationSlow ps = do - inv <- withNewTestChatCfg ps testCfgSlow "alice" aliceProfile $ \alice -> do - threadDelay 250000 - alice ##> "/connect" - getInvitationNoShortLink alice - withNewTestChatCfg ps testCfgSlow "bob" bobProfile $ \bob -> do - threadDelay 250000 - bob ##> ("/c " <> inv) - bob <## "confirmation sent!" - withTestChatCfg ps testCfgSlow "alice" $ \alice -> do - threadDelay 250000 - alice <## "subscribed 1 connections on server localhost" - alice ##> "/_set incognito :1 on" - alice <## "chat db error: SEPendingConnectionNotFound {connId = 1}" - withTestChatCfg ps testCfgSlow "bob" $ \bob -> do - bob <## "subscribed 1 connections on server localhost" - concurrently_ - (bob <## "alice (Alice): contact is connected") - (alice <## "bob (Bob): contact is connected") - alice <##> bob - alice `hasContactProfiles` ["alice", "bob"] - bob `hasContactProfiles` ["alice", "bob"] - testConnectionIncognitoUnchangedErrors :: HasCallStack => TestParams -> IO () testConnectionIncognitoUnchangedErrors = testChat2 aliceProfile bobProfile $ \alice bob -> do @@ -2022,8 +1956,14 @@ testChangePCCUser = testChat2 aliceProfile bobProfile $ alice ##> "/user alisa" showActiveUser alice "alisa" -- Change connection back to other user +#if defined(dbPostgres) + alice ##> "/_set conn user :2 3" + alice <## "connection 2 changed from user alisa to user alisa2, new link:" +#else + -- connection ID does not change in SQLite because table has no auto-increment alice ##> "/_set conn user :1 3" alice <## "connection 1 changed from user alisa to user alisa2, new link:" +#endif alice <## "" _shortInv <- getTermLine alice alice <## "" @@ -2065,8 +2005,14 @@ testChangePCCUserFromIncognito = testChat2 aliceProfile bobProfile $ alice ##> "/user alisa" showActiveUser alice "alisa" -- Change connection back to initial user +#if defined(dbPostgres) + alice ##> "/_set conn user :2 1" + alice <## "connection 2 changed from user alisa to user alice, new link:" +#else + -- connection ID does not change in SQLite because table has no auto-increment alice ##> "/_set conn user :1 1" alice <## "connection 1 changed from user alisa to user alice, new link:" +#endif alice <## "" _shortInv <- getTermLine alice alice <## "" @@ -2104,9 +2050,16 @@ testChangePCCUserAndThenIncognito = testChat2 aliceProfile bobProfile $ alice ##> "/user alisa" showActiveUser alice "alisa" -- Change connection to incognito and make sure it's attached to the newly created user profile +#if defined(dbPostgres) + alice ##> "/_set incognito :2 on" + _ <- getTermLine alice + alice <## "connection 2 changed to incognito" +#else + -- connection ID does not change in SQLite because table has no auto-increment alice ##> "/_set incognito :1 on" _ <- getTermLine alice alice <## "connection 1 changed to incognito" +#endif bob ##> ("/connect " <> inv) bob <## "confirmation sent!" alisaIncognito <- getTermLine alice @@ -2485,10 +2438,8 @@ testEnableTimedMessagesContact = alice #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(1, "Disappearing messages: enabled (1 sec)"), (1, "hi"), (0, "hey")]) bob #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(0, "Disappearing messages: enabled (1 sec)"), (0, "hi"), (1, "hey")]) threadDelay 1000000 - alice <## "timed message deleted: hi" - alice <## "timed message deleted: hey" - bob <## "timed message deleted: hi" - bob <## "timed message deleted: hey" + alice <### ["timed message deleted: hi", "timed message deleted: hey"] + bob <### ["timed message deleted: hi", "timed message deleted: hey"] alice #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(1, "Disappearing messages: enabled (1 sec)")]) bob #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(0, "Disappearing messages: enabled (1 sec)")]) -- turn off, messages are not disappearing @@ -2580,10 +2531,8 @@ testTimedMessagesEnabledGlobally = alice #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(0, "Disappearing messages: enabled (1 sec)"), (1, "hi"), (0, "hey")]) bob #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(1, "Disappearing messages: enabled (1 sec)"), (0, "hi"), (1, "hey")]) threadDelay 1000000 - alice <## "timed message deleted: hi" - bob <## "timed message deleted: hi" - alice <## "timed message deleted: hey" - bob <## "timed message deleted: hey" + alice <### ["timed message deleted: hi", "timed message deleted: hey"] + bob <### ["timed message deleted: hi", "timed message deleted: hey"] alice #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(0, "Disappearing messages: enabled (1 sec)")]) bob #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(1, "Disappearing messages: enabled (1 sec)")]) From 87e8a10f1e8aa0f46ead7cd768cfaa39e6d70e2f Mon Sep 17 00:00:00 2001 From: Evgeny Date: Mon, 5 Jan 2026 08:53:26 +0000 Subject: [PATCH 03/73] core: use strict tables (#6535) * core: use strict tables * fix field types * change encodings to match schema types; migrate sqlite tables to strict mode * stabilize postgres client tests, remove slow handshake tests * update simplexmq * fix test * change call_state type to text * fix directory service queries * update local_alias for existing schemas * change types before strict --- .../src/Directory/Service.hs | 10 +- simplex-chat.cabal | 2 + src/Simplex/Chat/Controller.hs | 9 +- src/Simplex/Chat/Messages/CIContent.hs | 12 +-- src/Simplex/Chat/Store/AppSettings.hs | 7 +- src/Simplex/Chat/Store/Postgres/Migrations.hs | 4 +- .../Migrations/M20251230_strict_tables.hs | 26 +++++ .../Store/Postgres/Migrations/chat_schema.sql | 2 +- src/Simplex/Chat/Store/SQLite/Migrations.hs | 4 +- .../Migrations/M20251230_strict_tables.hs | 70 ++++++++++++ .../SQLite/Migrations/agent_query_plans.txt | 4 - .../Store/SQLite/Migrations/chat_schema.sql | 102 +++++++++--------- src/Simplex/Chat/Types.hs | 23 ++-- src/Simplex/Chat/Types/Preferences.hs | 2 +- src/Simplex/Chat/Types/Shared.hs | 33 +++--- src/Simplex/Chat/View.hs | 14 +-- tests/ChatTests/Groups.hs | 2 +- tests/ChatTests/Utils.hs | 6 +- tests/SchemaDump.hs | 9 +- 19 files changed, 220 insertions(+), 121 deletions(-) create mode 100644 src/Simplex/Chat/Store/Postgres/Migrations/M20251230_strict_tables.hs create mode 100644 src/Simplex/Chat/Store/SQLite/Migrations/M20251230_strict_tables.hs diff --git a/apps/simplex-directory-service/src/Directory/Service.hs b/apps/simplex-directory-service/src/Directory/Service.hs index 5901e1d61a..4be7b6177e 100644 --- a/apps/simplex-directory-service/src/Directory/Service.hs +++ b/apps/simplex-directory-service/src/Directory/Service.hs @@ -665,7 +665,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName where rStatus = groupRolesStatus contactRole serviceRole groupRef = groupReference g - ctRole = "*" <> strEncodeTxt contactRole <> "*" + ctRole = "*" <> textEncode contactRole <> "*" suCtRole = "(user role is set to " <> ctRole <> ")." deServiceRoleChanged :: GroupInfo -> GroupMemberRole -> IO () @@ -691,7 +691,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName _ -> pure () where groupRef = groupReference g - srvRole = "*" <> strEncodeTxt serviceRole <> "*" + srvRole = "*" <> textEncode serviceRole <> "*" suSrvRole = "(" <> serviceName <> " role is changed to " <> srvRole <> ")." whenContactIsOwner gr action = getOwnerGroupMember groupId gr @@ -801,7 +801,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName let anotherRole = case acceptMemberRole of GRObserver -> GRMember; _ -> GRObserver sendReply $ initialRole n acceptMemberRole - <> ("Send /'role " <> tshow gId <> " " <> strEncodeTxt anotherRole <> "' to change it.\n\n") + <> ("Send /'role " <> tshow gId <> " " <> textEncode anotherRole <> "' to change it.\n\n") <> onlyViaLink gLink Left _ -> sendReply $ "Error: failed reading the initial member role for the group " <> n Just mRole -> do @@ -809,7 +809,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName Just gLink -> sendReply $ initialRole n mRole <> "\n" <> onlyViaLink gLink Nothing -> sendReply $ "Error: the initial member role for the group " <> n <> " was NOT upgated." where - initialRole n mRole = "The initial member role for the group " <> n <> " is set to *" <> strEncodeTxt mRole <> "*\n" + initialRole n mRole = "The initial member role for the group " <> n <> " is set to *" <> textEncode mRole <> "*\n" onlyViaLink gLink = "*Please note*: it applies only to members joining via this link: " <> groupLinkText gLink DCGroupFilter gId gName_ acceptance_ -> (if isAdmin then withGroupAndReg_ sendReply else withUserGroupReg_) gId gName_ $ \g _gr -> do @@ -852,7 +852,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName T.unlines $ [ "The link to join the group " <> groupRef <> ":", groupLinkText gLink, - "New member role: " <> strEncodeTxt acceptMemberRole + "New member role: " <> textEncode acceptMemberRole ] <> ["The link is being upgraded..." | shouldBeUpgraded] when shouldBeUpgraded $ do diff --git a/simplex-chat.cabal b/simplex-chat.cabal index 65bca0611f..95f386e9df 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -124,6 +124,7 @@ library Simplex.Chat.Store.Postgres.Migrations.M20251017_chat_tags_cascade Simplex.Chat.Store.Postgres.Migrations.M20251117_member_relations_vector Simplex.Chat.Store.Postgres.Migrations.M20251128_migrate_member_relations + Simplex.Chat.Store.Postgres.Migrations.M20251230_strict_tables else exposed-modules: Simplex.Chat.Archive @@ -271,6 +272,7 @@ library Simplex.Chat.Store.SQLite.Migrations.M20251017_chat_tags_cascade Simplex.Chat.Store.SQLite.Migrations.M20251117_member_relations_vector Simplex.Chat.Store.SQLite.Migrations.M20251128_migrate_member_relations + Simplex.Chat.Store.SQLite.Migrations.M20251230_strict_tables other-modules: Paths_simplex_chat hs-source-dirs: diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index 5754419933..30df251fbb 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -76,6 +76,7 @@ import Simplex.Messaging.Agent.Lock import Simplex.Messaging.Agent.Protocol import Simplex.Messaging.Agent.Store.Common (DBStore, withTransaction, withTransactionPriority) import Simplex.Messaging.Agent.Store.Shared (MigrationConfirmation, UpMigration) +import Simplex.Messaging.Agent.Store.DB (SQLError) import qualified Simplex.Messaging.Agent.Store.DB as DB import Simplex.Messaging.Client (HostMode (..), SMPProxyFallback (..), SMPProxyMode (..), SMPWebPortServers (..), SocksMode (..)) import qualified Simplex.Messaging.Crypto as C @@ -96,13 +97,7 @@ import Simplex.RemoteControl.Types import System.IO (Handle) import System.Mem.Weak (Weak) import UnliftIO.STM - -#if defined(dbPostgres) -import qualified Database.PostgreSQL.Simple as PSQL - -type SQLError = PSQL.SqlError -#else -import Database.SQLite.Simple (SQLError) +#if !defined(dbPostgres) import qualified Database.SQLite.Simple as SQL import Simplex.Messaging.Agent.Store.SQLite.DB (SlowQueryStats (..)) #endif diff --git a/src/Simplex/Chat/Messages/CIContent.hs b/src/Simplex/Chat/Messages/CIContent.hs index fd8f1cc41b..fedb6cd8e0 100644 --- a/src/Simplex/Chat/Messages/CIContent.hs +++ b/src/Simplex/Chat/Messages/CIContent.hs @@ -22,7 +22,7 @@ import qualified Data.Attoparsec.ByteString.Char8 as A import qualified Data.ByteString.Lazy as LB import Data.Int (Int64) import Data.Text (Text) -import Data.Text.Encoding (decodeLatin1, encodeUtf8) +import Data.Text.Encoding (encodeUtf8) import Data.Type.Equality import Data.Word (Word32) import Simplex.Chat.Messages.CIContent.Events @@ -323,7 +323,7 @@ e2eInfoPQText = ciGroupInvitationToText :: CIGroupInvitation -> GroupMemberRole -> Text ciGroupInvitationToText CIGroupInvitation {groupProfile = GroupProfile {displayName, fullName}} role = - "invitation to join group " <> displayName <> optionalFullName displayName fullName Nothing <> " as " <> (decodeLatin1 . strEncode $ role) + "invitation to join group " <> displayName <> optionalFullName displayName fullName Nothing <> " as " <> textEncode role rcvDirectEventToText :: RcvDirectEvent -> Text rcvDirectEventToText = \case @@ -338,9 +338,9 @@ rcvGroupEventToText = \case RGEMemberAccepted _ p -> "accepted " <> profileToText p RGEUserAccepted -> "accepted you" RGEMemberLeft -> "left" - RGEMemberRole _ p r -> "changed role of " <> profileToText p <> " to " <> safeDecodeUtf8 (strEncode r) + RGEMemberRole _ p r -> "changed role of " <> profileToText p <> " to " <> textEncode r RGEMemberBlocked _ p blocked -> (if blocked then "blocked" else "unblocked") <> " " <> profileToText p - RGEUserRole r -> "changed your role to " <> safeDecodeUtf8 (strEncode r) + RGEUserRole r -> "changed your role to " <> textEncode r RGEMemberDeleted _ p -> "removed " <> profileToText p RGEUserDeleted -> "removed you" RGEGroupDeleted -> "deleted group" @@ -352,9 +352,9 @@ rcvGroupEventToText = \case sndGroupEventToText :: SndGroupEvent -> Text sndGroupEventToText = \case - SGEMemberRole _ p r -> "changed role of " <> profileToText p <> " to " <> safeDecodeUtf8 (strEncode r) + SGEMemberRole _ p r -> "changed role of " <> profileToText p <> " to " <> textEncode r SGEMemberBlocked _ p blocked -> (if blocked then "blocked" else "unblocked") <> " " <> profileToText p - SGEUserRole r -> "changed role for yourself to " <> safeDecodeUtf8 (strEncode r) + SGEUserRole r -> "changed role for yourself to " <> textEncode r SGEMemberDeleted _ p -> "removed " <> profileToText p SGEUserLeft -> "left" SGEGroupUpdated _ -> "group profile updated" diff --git a/src/Simplex/Chat/Store/AppSettings.hs b/src/Simplex/Chat/Store/AppSettings.hs index dbdd538cf4..4ee55b2143 100644 --- a/src/Simplex/Chat/Store/AppSettings.hs +++ b/src/Simplex/Chat/Store/AppSettings.hs @@ -5,11 +5,12 @@ module Simplex.Chat.Store.AppSettings where import Control.Monad (join) import Control.Monad.IO.Class (liftIO) -import qualified Data.Aeson as J import Data.Maybe (fromMaybe) import Simplex.Chat.AppSettings (AppSettings (..), combineAppSettings, defaultAppSettings, defaultParseAppSettings) import Simplex.Messaging.Agent.Store.AgentStore (maybeFirstRow) import qualified Simplex.Messaging.Agent.Store.DB as DB +import Simplex.Messaging.Util (decodeJSON, encodeJSON) + #if defined(dbPostgres) import Database.PostgreSQL.Simple (Only (..)) #else @@ -19,9 +20,9 @@ import Database.SQLite.Simple (Only (..)) saveAppSettings :: DB.Connection -> AppSettings -> IO () saveAppSettings db appSettings = do DB.execute_ db "DELETE FROM app_settings" - DB.execute db "INSERT INTO app_settings (app_settings) VALUES (?)" (Only $ J.encode appSettings) + DB.execute db "INSERT INTO app_settings (app_settings) VALUES (?)" (Only $ encodeJSON appSettings) getAppSettings :: DB.Connection -> Maybe AppSettings -> IO AppSettings getAppSettings db platformDefaults = do - stored_ <- join <$> liftIO (maybeFirstRow (J.decodeStrict . fromOnly) $ DB.query_ db "SELECT app_settings FROM app_settings") + stored_ <- join <$> liftIO (maybeFirstRow (decodeJSON . fromOnly) $ DB.query_ db "SELECT app_settings FROM app_settings") pure $ combineAppSettings (fromMaybe defaultAppSettings platformDefaults) (fromMaybe defaultParseAppSettings stored_) diff --git a/src/Simplex/Chat/Store/Postgres/Migrations.hs b/src/Simplex/Chat/Store/Postgres/Migrations.hs index 5fd2607f48..ef6d96bd45 100644 --- a/src/Simplex/Chat/Store/Postgres/Migrations.hs +++ b/src/Simplex/Chat/Store/Postgres/Migrations.hs @@ -23,6 +23,7 @@ import Simplex.Chat.Store.Postgres.Migrations.M20251007_connections_sync import Simplex.Chat.Store.Postgres.Migrations.M20251017_chat_tags_cascade import Simplex.Chat.Store.Postgres.Migrations.M20251117_member_relations_vector import Simplex.Chat.Store.Postgres.Migrations.M20251128_migrate_member_relations +import Simplex.Chat.Store.Postgres.Migrations.M20251230_strict_tables import Simplex.Messaging.Agent.Store.Shared (Migration (..)) schemaMigrations :: [(String, Text, Maybe Text)] @@ -45,7 +46,8 @@ schemaMigrations = ("20251007_connections_sync", m20251007_connections_sync, Just down_m20251007_connections_sync), ("20251017_chat_tags_cascade", m20251017_chat_tags_cascade, Just down_m20251017_chat_tags_cascade), ("20251117_member_relations_vector", m20251117_member_relations_vector, Just down_m20251117_member_relations_vector), - ("20251128_migrate_member_relations", m20251128_migrate_member_relations, Just down_m20251128_migrate_member_relations) + ("20251128_migrate_member_relations", m20251128_migrate_member_relations, Just down_m20251128_migrate_member_relations), + ("20251230_strict_tables", m20251230_strict_tables, Just down_m20251230_strict_tables) ] -- | The list of migrations in ascending order by date diff --git a/src/Simplex/Chat/Store/Postgres/Migrations/M20251230_strict_tables.hs b/src/Simplex/Chat/Store/Postgres/Migrations/M20251230_strict_tables.hs new file mode 100644 index 0000000000..81bc789ceb --- /dev/null +++ b/src/Simplex/Chat/Store/Postgres/Migrations/M20251230_strict_tables.hs @@ -0,0 +1,26 @@ +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Store.Postgres.Migrations.M20251230_strict_tables where + +import Data.Text (Text) +import Text.RawString.QQ (r) +import Simplex.Messaging.Agent.Store.Postgres.Migrations.M20251230_strict_tables (isValidText) + +m20251230_strict_tables :: Text +m20251230_strict_tables = + isValidText + <> [r| +DELETE FROM calls +WHERE NOT simplex_is_valid_text(call_state); + +ALTER TABLE calls ALTER COLUMN call_state TYPE TEXT USING call_state::TEXT; + +DROP FUNCTION simplex_is_valid_text(BYTEA); +|] + +down_m20251230_strict_tables :: Text +down_m20251230_strict_tables = + [r| +ALTER TABLE calls ALTER COLUMN call_state TYPE BYTEA USING call_state::BYTEA; +|] diff --git a/src/Simplex/Chat/Store/Postgres/Migrations/chat_schema.sql b/src/Simplex/Chat/Store/Postgres/Migrations/chat_schema.sql index a3ca7693a6..238a6bfdbb 100644 --- a/src/Simplex/Chat/Store/Postgres/Migrations/chat_schema.sql +++ b/src/Simplex/Chat/Store/Postgres/Migrations/chat_schema.sql @@ -173,7 +173,7 @@ CREATE TABLE test_chat_schema.calls ( contact_id bigint NOT NULL, shared_call_id bytea NOT NULL, chat_item_id bigint NOT NULL, - call_state bytea NOT NULL, + call_state text NOT NULL, call_ts timestamp with time zone NOT NULL, user_id bigint NOT NULL, created_at timestamp with time zone DEFAULT now() NOT NULL, diff --git a/src/Simplex/Chat/Store/SQLite/Migrations.hs b/src/Simplex/Chat/Store/SQLite/Migrations.hs index 2e5866beae..f740c5654f 100644 --- a/src/Simplex/Chat/Store/SQLite/Migrations.hs +++ b/src/Simplex/Chat/Store/SQLite/Migrations.hs @@ -146,6 +146,7 @@ import Simplex.Chat.Store.SQLite.Migrations.M20251007_connections_sync import Simplex.Chat.Store.SQLite.Migrations.M20251017_chat_tags_cascade import Simplex.Chat.Store.SQLite.Migrations.M20251117_member_relations_vector import Simplex.Chat.Store.SQLite.Migrations.M20251128_migrate_member_relations +import Simplex.Chat.Store.SQLite.Migrations.M20251230_strict_tables import Simplex.Messaging.Agent.Store.Shared (Migration (..)) schemaMigrations :: [(String, Query, Maybe Query)] @@ -291,7 +292,8 @@ schemaMigrations = ("20251007_connections_sync", m20251007_connections_sync, Just down_m20251007_connections_sync), ("20251017_chat_tags_cascade", m20251017_chat_tags_cascade, Just down_m20251017_chat_tags_cascade), ("20251117_member_relations_vector", m20251117_member_relations_vector, Just down_m20251117_member_relations_vector), - ("20251128_migrate_member_relations", m20251128_migrate_member_relations, Just down_m20251128_migrate_member_relations) + ("20251128_migrate_member_relations", m20251128_migrate_member_relations, Just down_m20251128_migrate_member_relations), + ("20251230_strict_tables", m20251230_strict_tables, Just down_m20251230_strict_tables) ] -- | The list of migrations in ascending order by date diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/M20251230_strict_tables.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20251230_strict_tables.hs new file mode 100644 index 0000000000..be92da8f5e --- /dev/null +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20251230_strict_tables.hs @@ -0,0 +1,70 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Store.SQLite.Migrations.M20251230_strict_tables where + +import Database.SQLite.Simple (Query) +import Database.SQLite.Simple.QQ (sql) + +m20251230_strict_tables :: Query +m20251230_strict_tables = + [sql| +UPDATE group_members +SET member_role = CAST(member_role as TEXT), + member_restriction = CAST(member_restriction as TEXT); + +UPDATE user_contact_links SET group_link_member_role = CAST(group_link_member_role as TEXT); + +UPDATE app_settings SET app_settings = CAST(app_settings as TEXT); + +PRAGMA writable_schema=1; + +UPDATE sqlite_master +SET sql = replace(sql, 'call_state BLOB NOT NULL', 'call_state TEXT NOT NULL') +WHERE type = 'table' AND name = 'calls'; + +UPDATE sqlite_master +SET sql = replace(sql, 'local_alias DEFAULT', 'local_alias TEXT DEFAULT') +WHERE type = 'table' AND name = 'connections'; + +UPDATE sqlite_master +SET sql = CASE + WHEN LOWER(SUBSTR(sql, -15)) = ') without rowid' THEN sql || ', STRICT' + WHEN SUBSTR(sql, -1) = ')' THEN sql || ' STRICT' + ELSE sql +END +WHERE type = 'table' AND name != 'sqlite_sequence'; + +PRAGMA writable_schema=0; +|] + +down_m20251230_strict_tables :: Query +down_m20251230_strict_tables = + [sql| +PRAGMA writable_schema=1; + +UPDATE sqlite_master +SET sql = CASE + WHEN LOWER(SUBSTR(sql, -8)) = ', strict' THEN SUBSTR(sql, 1, LENGTH(sql) - 8) + WHEN LOWER(SUBSTR(sql, -7)) = ' strict' THEN SUBSTR(sql, 1, LENGTH(sql) - 7) + ELSE sql +END +WHERE type = 'table' AND name != 'sqlite_sequence'; + +UPDATE sqlite_master +SET sql = replace(sql, 'call_state TEXT NOT NULL', 'call_state BLOB NOT NULL') +WHERE type = 'table' AND name = 'calls'; + +UPDATE sqlite_master +SET sql = replace(sql, 'local_alias TEXT DEFAULT', 'local_alias DEFAULT') +WHERE type = 'table' AND name = 'connections'; + +PRAGMA writable_schema=0; + +UPDATE group_members +SET member_role = CAST(member_role as BLOB), + member_restriction = CAST(member_restriction as BLOB); + +UPDATE user_contact_links SET group_link_member_role = CAST(group_link_member_role as BLOB); + +UPDATE app_settings SET app_settings = CAST(app_settings as BLOB); +|] diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/agent_query_plans.txt b/src/Simplex/Chat/Store/SQLite/Migrations/agent_query_plans.txt index efbadf7c53..6d3f69d5fa 100644 --- a/src/Simplex/Chat/Store/SQLite/Migrations/agent_query_plans.txt +++ b/src/Simplex/Chat/Store/SQLite/Migrations/agent_query_plans.txt @@ -1255,10 +1255,6 @@ Query: UPDATE snd_file_chunk_replicas SET replica_status = ?, updated_at = ? WHE Plan: SEARCH snd_file_chunk_replicas USING INTEGER PRIMARY KEY (rowid=?) -Query: UPDATE snd_files SET deleted = 1, updated_at = ? WHERE snd_file_id = ? -Plan: -SEARCH snd_files USING INTEGER PRIMARY KEY (rowid=?) - Query: UPDATE snd_files SET prefix_path = NULL, status = ?, updated_at = ? WHERE snd_file_id = ? Plan: SEARCH snd_files USING INTEGER PRIMARY KEY (rowid=?) diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql b/src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql index 1e2db113c3..fd4fa914d0 100644 --- a/src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql +++ b/src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql @@ -2,7 +2,7 @@ CREATE TABLE migrations( name TEXT NOT NULL PRIMARY KEY, ts TEXT NOT NULL, down TEXT -); +) STRICT; CREATE TABLE contact_profiles( -- remote user profile contact_profile_id INTEGER PRIMARY KEY, @@ -20,7 +20,7 @@ CREATE TABLE contact_profiles( contact_link BLOB, short_descr TEXT, chat_peer_type TEXT -); +) STRICT; CREATE TABLE users( user_id INTEGER PRIMARY KEY, contact_id INTEGER NOT NULL UNIQUE REFERENCES contacts ON DELETE RESTRICT @@ -44,7 +44,7 @@ CREATE TABLE users( ON DELETE RESTRICT ON UPDATE CASCADE DEFERRABLE INITIALLY DEFERRED -); +) STRICT; CREATE TABLE display_names( user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE, local_display_name TEXT NOT NULL, @@ -54,7 +54,7 @@ CREATE TABLE display_names( updated_at TEXT CHECK(updated_at NOT NULL), PRIMARY KEY(user_id, local_display_name) ON CONFLICT FAIL, UNIQUE(user_id, ldn_base, ldn_suffix) ON CONFLICT FAIL -) WITHOUT ROWID; +) WITHOUT ROWID, STRICT; CREATE TABLE contacts( contact_id INTEGER PRIMARY KEY, contact_profile_id INTEGER REFERENCES contact_profiles ON DELETE SET NULL, @@ -96,7 +96,7 @@ CREATE TABLE contacts( ON UPDATE CASCADE, UNIQUE(user_id, local_display_name), UNIQUE(user_id, contact_profile_id) -); +) STRICT; CREATE TABLE known_servers( server_id INTEGER PRIMARY KEY, host TEXT NOT NULL, @@ -106,7 +106,7 @@ CREATE TABLE known_servers( created_at TEXT CHECK(created_at NOT NULL), updated_at TEXT CHECK(updated_at NOT NULL), UNIQUE(user_id, host, port) -) WITHOUT ROWID; +) WITHOUT ROWID, STRICT; CREATE TABLE group_profiles( -- shared group profiles group_profile_id INTEGER PRIMARY KEY, @@ -122,7 +122,7 @@ CREATE TABLE group_profiles( description TEXT NULL, member_admission TEXT, short_descr TEXT -); +) STRICT; CREATE TABLE groups( group_id INTEGER PRIMARY KEY, -- local group ID user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE, @@ -163,7 +163,7 @@ CREATE TABLE groups( ON UPDATE CASCADE, UNIQUE(user_id, local_display_name), UNIQUE(user_id, group_profile_id) -); +) STRICT; CREATE TABLE group_members( -- group members, excluding the local user group_member_id INTEGER PRIMARY KEY, @@ -203,7 +203,7 @@ CREATE TABLE group_members( ON DELETE CASCADE ON UPDATE CASCADE, UNIQUE(group_id, member_id) -); +) STRICT; CREATE TABLE group_member_intros( group_member_intro_id INTEGER PRIMARY KEY, re_group_member_id INTEGER NOT NULL REFERENCES group_members(group_member_id) ON DELETE CASCADE, @@ -215,7 +215,7 @@ CREATE TABLE group_member_intros( updated_at TEXT CHECK(updated_at NOT NULL), intro_chat_protocol_version INTEGER NOT NULL DEFAULT 3, -- see GroupMemberIntroStatus UNIQUE(re_group_member_id, to_group_member_id) -); +) STRICT; CREATE TABLE files( file_id INTEGER PRIMARY KEY, contact_id INTEGER REFERENCES contacts ON DELETE CASCADE, @@ -240,7 +240,7 @@ CREATE TABLE files( file_crypto_nonce BLOB, note_folder_id INTEGER DEFAULT NULL REFERENCES note_folders ON DELETE CASCADE, redirect_file_id INTEGER REFERENCES files ON DELETE CASCADE -); +) STRICT; CREATE TABLE snd_files( file_id INTEGER NOT NULL REFERENCES files ON DELETE CASCADE, connection_id INTEGER NOT NULL REFERENCES connections ON DELETE CASCADE, @@ -253,7 +253,7 @@ CREATE TABLE snd_files( file_descr_id INTEGER NULL REFERENCES xftp_file_descriptions ON DELETE SET NULL, PRIMARY KEY(file_id, connection_id) -) WITHOUT ROWID; +) WITHOUT ROWID, STRICT; CREATE TABLE rcv_files( file_id INTEGER PRIMARY KEY REFERENCES files ON DELETE CASCADE, file_status TEXT NOT NULL, -- new, accepted, connected, completed @@ -270,7 +270,7 @@ CREATE TABLE rcv_files( agent_rcv_file_deleted INTEGER DEFAULT 0 CHECK(agent_rcv_file_deleted NOT NULL), to_receive INTEGER, user_approved_relays INTEGER NOT NULL DEFAULT 0 -); +) STRICT; CREATE TABLE rcv_file_chunks( file_id INTEGER NOT NULL REFERENCES rcv_files ON DELETE CASCADE, chunk_number INTEGER NOT NULL, @@ -279,7 +279,7 @@ CREATE TABLE rcv_file_chunks( created_at TEXT CHECK(created_at NOT NULL), updated_at TEXT CHECK(updated_at NOT NULL), -- 0(received), 1(appended to file) PRIMARY KEY(file_id, chunk_number) -) WITHOUT ROWID; +) WITHOUT ROWID, STRICT; CREATE TABLE connections( -- all SMP agent connections connection_id INTEGER PRIMARY KEY, @@ -302,7 +302,7 @@ CREATE TABLE connections( REFERENCES user_contact_links(user_contact_link_id) ON DELETE SET NULL, custom_user_profile_id INTEGER REFERENCES contact_profiles ON DELETE SET NULL, conn_req_inv BLOB, - local_alias DEFAULT '' CHECK(local_alias NOT NULL), + local_alias TEXT DEFAULT '' CHECK(local_alias NOT NULL), via_group_link INTEGER DEFAULT 0 CHECK(via_group_link NOT NULL), group_link_id BLOB, security_code TEXT NULL, @@ -325,7 +325,7 @@ CREATE TABLE connections( REFERENCES snd_files(file_id, connection_id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED -); +) STRICT; CREATE TABLE user_contact_links( user_contact_link_id INTEGER PRIMARY KEY, conn_req_contact BLOB NOT NULL, @@ -344,7 +344,7 @@ CREATE TABLE user_contact_links( short_link_data_set INTEGER NOT NULL DEFAULT 0, short_link_large_data_set INTEGER NOT NULL DEFAULT 0, UNIQUE(user_id, local_display_name) -); +) STRICT; CREATE TABLE contact_requests( contact_request_id INTEGER PRIMARY KEY, user_contact_link_id INTEGER REFERENCES user_contact_links @@ -372,7 +372,7 @@ CREATE TABLE contact_requests( DEFERRABLE INITIALLY DEFERRED, UNIQUE(user_id, local_display_name), UNIQUE(user_id, contact_profile_id) -); +) STRICT; CREATE TABLE messages( message_id INTEGER PRIMARY KEY, msg_sent INTEGER NOT NULL, -- 0 for received, 1 for sent @@ -388,14 +388,14 @@ CREATE TABLE messages( author_group_member_id INTEGER REFERENCES group_members ON DELETE SET NULL, forwarded_by_group_member_id INTEGER REFERENCES group_members ON DELETE SET NULL, broker_ts TEXT -); +) STRICT; CREATE TABLE pending_group_messages( pending_group_message_id INTEGER PRIMARY KEY, group_member_id INTEGER NOT NULL REFERENCES group_members ON DELETE CASCADE, message_id INTEGER NOT NULL REFERENCES messages ON DELETE CASCADE, created_at TEXT NOT NULL DEFAULT(datetime('now')), updated_at TEXT NOT NULL DEFAULT(datetime('now')) -); +) STRICT; CREATE TABLE chat_items( chat_item_id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE, @@ -441,7 +441,7 @@ CREATE TABLE chat_items( group_scope_tag TEXT, group_scope_group_member_id INTEGER REFERENCES group_members(group_member_id) ON DELETE CASCADE, show_group_as_sender INTEGER NOT NULL DEFAULT 0 -); +) STRICT; CREATE TABLE sqlite_sequence(name,seq); CREATE TABLE chat_item_messages( chat_item_id INTEGER NOT NULL REFERENCES chat_items ON DELETE CASCADE, @@ -449,21 +449,21 @@ CREATE TABLE chat_item_messages( created_at TEXT NOT NULL DEFAULT(datetime('now')), updated_at TEXT NOT NULL DEFAULT(datetime('now')), UNIQUE(chat_item_id, message_id) -); +) STRICT; CREATE TABLE calls( -- stores call invitations state for communicating state between NSE and app when call notification comes call_id INTEGER PRIMARY KEY, contact_id INTEGER NOT NULL REFERENCES contacts ON DELETE CASCADE, shared_call_id BLOB NOT NULL, chat_item_id INTEGER NOT NULL REFERENCES chat_items ON DELETE CASCADE, - call_state BLOB NOT NULL, + call_state TEXT NOT NULL, call_ts TEXT NOT NULL, user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE, created_at TEXT NOT NULL DEFAULT(datetime('now')), updated_at TEXT NOT NULL DEFAULT(datetime('now')) , call_uuid TEXT NOT NULL DEFAULT "" -); +) STRICT; CREATE TABLE commands( command_id INTEGER PRIMARY KEY AUTOINCREMENT, -- used as ACorrId connection_id INTEGER REFERENCES connections ON DELETE CASCADE, @@ -472,14 +472,14 @@ CREATE TABLE commands( user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE, created_at TEXT NOT NULL DEFAULT(datetime('now')), updated_at TEXT NOT NULL DEFAULT(datetime('now')) -); +) STRICT; CREATE TABLE settings( settings_id INTEGER PRIMARY KEY, chat_item_ttl INTEGER, user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE, created_at TEXT NOT NULL DEFAULT(datetime('now')), updated_at TEXT NOT NULL DEFAULT(datetime('now')) -); +) STRICT; CREATE TABLE IF NOT EXISTS "protocol_servers"( smp_server_id INTEGER PRIMARY KEY, host TEXT NOT NULL, @@ -494,7 +494,7 @@ CREATE TABLE IF NOT EXISTS "protocol_servers"( updated_at TEXT NOT NULL DEFAULT(datetime('now')), protocol TEXT NOT NULL DEFAULT 'smp', UNIQUE(user_id, host, port) -); +) STRICT; CREATE TABLE xftp_file_descriptions( file_descr_id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE, @@ -503,7 +503,7 @@ CREATE TABLE xftp_file_descriptions( file_descr_complete INTEGER NOT NULL DEFAULT(0), created_at TEXT NOT NULL DEFAULT(datetime('now')), updated_at TEXT NOT NULL DEFAULT(datetime('now')) -); +) STRICT; CREATE TABLE extra_xftp_file_descriptions( extra_file_descr_id INTEGER PRIMARY KEY, file_id INTEGER NOT NULL REFERENCES files ON DELETE CASCADE, @@ -511,7 +511,7 @@ CREATE TABLE extra_xftp_file_descriptions( file_descr_text TEXT NOT NULL, created_at TEXT NOT NULL DEFAULT(datetime('now')), updated_at TEXT NOT NULL DEFAULT(datetime('now')) -); +) STRICT; CREATE TABLE chat_item_versions( -- contains versions only for edited chat items, including current version chat_item_version_id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -520,7 +520,7 @@ CREATE TABLE chat_item_versions( item_version_ts TEXT NOT NULL, created_at TEXT NOT NULL DEFAULT(datetime('now')), updated_at TEXT NOT NULL DEFAULT(datetime('now')) -); +) STRICT; CREATE TABLE chat_item_reactions( chat_item_reaction_id INTEGER PRIMARY KEY AUTOINCREMENT, item_member_id BLOB, -- member that created item, NULL for items in direct chats @@ -534,7 +534,7 @@ CREATE TABLE chat_item_reactions( reaction_ts TEXT NOT NULL, -- broker_ts of creating message for received, created_at for sent created_at TEXT NOT NULL DEFAULT(datetime('now')), updated_at TEXT NOT NULL DEFAULT(datetime('now')) -); +) STRICT; CREATE TABLE chat_item_moderations( chat_item_moderation_id INTEGER PRIMARY KEY, group_id INTEGER NOT NULL REFERENCES groups ON DELETE CASCADE, @@ -545,7 +545,7 @@ CREATE TABLE chat_item_moderations( moderated_at TEXT NOT NULL, -- broker_ts of creating message created_at TEXT NOT NULL DEFAULT(datetime('now')), updated_at TEXT NOT NULL DEFAULT(datetime('now')) -); +) STRICT; CREATE TABLE group_snd_item_statuses( group_snd_item_status_id INTEGER PRIMARY KEY, chat_item_id INTEGER NOT NULL REFERENCES chat_items ON DELETE CASCADE, @@ -555,7 +555,7 @@ CREATE TABLE group_snd_item_statuses( updated_at TEXT NOT NULL DEFAULT(datetime('now')) , via_proxy INTEGER -); +) STRICT; CREATE TABLE IF NOT EXISTS "sent_probes"( sent_probe_id INTEGER PRIMARY KEY, contact_id INTEGER REFERENCES contacts ON DELETE CASCADE, @@ -565,7 +565,7 @@ CREATE TABLE IF NOT EXISTS "sent_probes"( created_at TEXT CHECK(created_at NOT NULL), updated_at TEXT CHECK(updated_at NOT NULL), UNIQUE(user_id, probe) -); +) STRICT; CREATE TABLE IF NOT EXISTS "sent_probe_hashes"( sent_probe_hash_id INTEGER PRIMARY KEY, sent_probe_id INTEGER NOT NULL REFERENCES "sent_probes" ON DELETE CASCADE, @@ -574,7 +574,7 @@ CREATE TABLE IF NOT EXISTS "sent_probe_hashes"( user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE, created_at TEXT CHECK(created_at NOT NULL), updated_at TEXT CHECK(updated_at NOT NULL) -); +) STRICT; CREATE TABLE IF NOT EXISTS "received_probes"( received_probe_id INTEGER PRIMARY KEY, contact_id INTEGER REFERENCES contacts ON DELETE CASCADE, @@ -584,7 +584,7 @@ CREATE TABLE IF NOT EXISTS "received_probes"( user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE, created_at TEXT CHECK(created_at NOT NULL), updated_at TEXT CHECK(updated_at NOT NULL) -); +) STRICT; CREATE TABLE remote_hosts( -- e.g., mobiles known to a desktop app remote_host_id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -599,7 +599,7 @@ CREATE TABLE remote_hosts( bind_addr TEXT, bind_iface TEXT, bind_port INTEGER -); +) STRICT; CREATE TABLE remote_controllers( -- e.g., desktops known to a mobile app remote_ctrl_id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -610,7 +610,7 @@ CREATE TABLE remote_controllers( id_pub BLOB NOT NULL, -- remote controller long-term/identity key to verify signatures dh_priv_key BLOB NOT NULL, -- last session DH key prev_dh_priv_key BLOB -- previous session DH key -); +) STRICT; CREATE TABLE IF NOT EXISTS "msg_deliveries"( msg_delivery_id INTEGER PRIMARY KEY, message_id INTEGER NOT NULL REFERENCES messages ON DELETE CASCADE, -- non UNIQUE for group messages and for batched messages @@ -621,7 +621,7 @@ CREATE TABLE IF NOT EXISTS "msg_deliveries"( created_at TEXT CHECK(created_at NOT NULL), updated_at TEXT CHECK(updated_at NOT NULL), delivery_status TEXT -- MsgDeliveryStatus -); +) STRICT; CREATE TABLE note_folders( note_folder_id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE, @@ -630,8 +630,8 @@ CREATE TABLE note_folders( chat_ts TEXT NOT NULL DEFAULT(datetime('now')), favorite INTEGER NOT NULL DEFAULT 0, unread_chat INTEGER NOT NULL DEFAULT 0 -); -CREATE TABLE app_settings(app_settings TEXT NOT NULL); +) STRICT; +CREATE TABLE app_settings(app_settings TEXT NOT NULL) STRICT; CREATE TABLE server_operators( server_operator_id INTEGER PRIMARY KEY AUTOINCREMENT, server_operator_tag TEXT, @@ -645,14 +645,14 @@ CREATE TABLE server_operators( xftp_role_proxy INTEGER NOT NULL DEFAULT 1, created_at TEXT NOT NULL DEFAULT(datetime('now')), updated_at TEXT NOT NULL DEFAULT(datetime('now')) -); +) STRICT; CREATE TABLE usage_conditions( usage_conditions_id INTEGER PRIMARY KEY AUTOINCREMENT, conditions_commit TEXT NOT NULL UNIQUE, notified_at TEXT, created_at TEXT NOT NULL DEFAULT(datetime('now')), updated_at TEXT NOT NULL DEFAULT(datetime('now')) -); +) STRICT; CREATE TABLE operator_usage_conditions( operator_usage_conditions_id INTEGER PRIMARY KEY AUTOINCREMENT, server_operator_id INTEGER REFERENCES server_operators(server_operator_id) ON DELETE SET NULL ON UPDATE CASCADE, @@ -662,26 +662,26 @@ CREATE TABLE operator_usage_conditions( created_at TEXT NOT NULL DEFAULT(datetime('now')) , auto_accepted INTEGER DEFAULT 0 -); +) STRICT; CREATE TABLE chat_tags( chat_tag_id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER REFERENCES users ON DELETE CASCADE, chat_tag_text TEXT NOT NULL, chat_tag_emoji TEXT, tag_order INTEGER NOT NULL -); +) STRICT; CREATE TABLE chat_tags_chats( contact_id INTEGER REFERENCES contacts ON DELETE CASCADE, group_id INTEGER REFERENCES groups ON DELETE CASCADE, chat_tag_id INTEGER NOT NULL REFERENCES chat_tags ON DELETE CASCADE -); +) STRICT; CREATE TABLE chat_item_mentions( chat_item_mention_id INTEGER PRIMARY KEY AUTOINCREMENT, group_id INTEGER NOT NULL REFERENCES groups ON DELETE CASCADE, member_id BLOB NOT NULL, chat_item_id INTEGER NOT NULL REFERENCES chat_items ON DELETE CASCADE, display_name TEXT NOT NULL -); +) STRICT; CREATE TABLE delivery_tasks( delivery_task_id INTEGER PRIMARY KEY AUTOINCREMENT, group_id INTEGER NOT NULL REFERENCES groups ON DELETE CASCADE, @@ -697,7 +697,7 @@ CREATE TABLE delivery_tasks( failed INTEGER DEFAULT 0, created_at TEXT NOT NULL DEFAULT(datetime('now')), updated_at TEXT NOT NULL DEFAULT(datetime('now')) -); +) STRICT; CREATE TABLE delivery_jobs( delivery_job_id INTEGER PRIMARY KEY AUTOINCREMENT, group_id INTEGER NOT NULL REFERENCES groups ON DELETE CASCADE, @@ -713,16 +713,16 @@ CREATE TABLE delivery_jobs( failed INTEGER DEFAULT 0, created_at TEXT NOT NULL DEFAULT(datetime('now')), updated_at TEXT NOT NULL DEFAULT(datetime('now')) -); +) STRICT; CREATE TABLE group_member_status_predicates( member_status TEXT NOT NULL PRIMARY KEY, current_member INTEGER NOT NULL DEFAULT 0 -); +) STRICT; CREATE TABLE connections_sync( connections_sync_id INTEGER PRIMARY KEY AUTOINCREMENT, should_sync INTEGER NOT NULL DEFAULT 0, last_sync_ts TEXT -); +) STRICT; CREATE INDEX contact_profiles_index ON contact_profiles( display_name, full_name diff --git a/src/Simplex/Chat/Types.hs b/src/Simplex/Chat/Types.hs index 7aba1c2ad5..44ea46492c 100644 --- a/src/Simplex/Chat/Types.hs +++ b/src/Simplex/Chat/Types.hs @@ -58,7 +58,7 @@ import Simplex.Messaging.Crypto.File (CryptoFileArgs (..)) import Simplex.Messaging.Crypto.Ratchet (PQEncryption (..), PQSupport, pattern PQEncOff) import Simplex.Messaging.Encoding.String import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON, sumTypeJSON) -import Simplex.Messaging.Util (decodeJSON, encodeJSON, safeDecodeUtf8, (<$?>)) +import Simplex.Messaging.Util (decodeJSON, encodeJSON, safeDecodeUtf8) import Simplex.Messaging.Version import Simplex.Messaging.Version.Internal #if defined(dbPostgres) @@ -871,27 +871,26 @@ data MemberRestrictionStatus | MRSUnknown Text deriving (Eq, Show) -instance FromField MemberRestrictionStatus where fromField = blobFieldDecoder strDecode +instance FromField MemberRestrictionStatus where fromField = fromTextField_ textDecode -instance ToField MemberRestrictionStatus where toField = toField . strEncode +instance ToField MemberRestrictionStatus where toField = toField . textEncode -instance StrEncoding MemberRestrictionStatus where - strEncode = \case +instance TextEncoding MemberRestrictionStatus where + textEncode = \case MRSBlocked -> "blocked" MRSUnrestricted -> "unrestricted" - MRSUnknown tag -> encodeUtf8 tag - strDecode s = Right $ case s of + MRSUnknown tag -> tag + textDecode s = Just $ case s of "blocked" -> MRSBlocked "unrestricted" -> MRSUnrestricted - tag -> MRSUnknown $ safeDecodeUtf8 tag - strP = strDecode <$?> A.takeByteString + tag -> MRSUnknown tag instance FromJSON MemberRestrictionStatus where - parseJSON = strParseJSON "MemberRestrictionStatus" + parseJSON = textParseJSON "MemberRestrictionStatus" instance ToJSON MemberRestrictionStatus where - toJSON = strToJSON - toEncoding = strToJEncoding + toJSON = textToJSON + toEncoding = textToEncoding mrsBlocked :: MemberRestrictionStatus -> Bool mrsBlocked = \case diff --git a/src/Simplex/Chat/Types/Preferences.hs b/src/Simplex/Chat/Types/Preferences.hs index f85e910bf8..d8c6f10b3a 100644 --- a/src/Simplex/Chat/Types/Preferences.hs +++ b/src/Simplex/Chat/Types/Preferences.hs @@ -782,7 +782,7 @@ groupPrefStateText :: HasField "enable" p GroupFeatureEnabled => GroupFeature -> groupPrefStateText feature pref param role = let enabled = getField @"enable" pref paramText = if enabled == FEOn then groupParamText_ feature param else "" - roleText = maybe "" (\r -> " for " <> safeDecodeUtf8 (strEncode r) <> "s") role + roleText = maybe "" (\r -> " for " <> textEncode r <> "s") role in groupFeatureNameText feature <> ": " <> safeDecodeUtf8 (strEncode enabled) <> paramText <> roleText groupParamText_ :: GroupFeature -> Maybe Int -> Text diff --git a/src/Simplex/Chat/Types/Shared.hs b/src/Simplex/Chat/Types/Shared.hs index 60ebe9d033..280fc32ea4 100644 --- a/src/Simplex/Chat/Types/Shared.hs +++ b/src/Simplex/Chat/Types/Shared.hs @@ -7,7 +7,7 @@ import Data.Aeson (FromJSON (..), ToJSON (..)) import qualified Data.Attoparsec.ByteString.Char8 as A import qualified Data.ByteString.Char8 as B import Simplex.Chat.Options.DB (FromField (..), ToField (..)) -import Simplex.Messaging.Agent.Store.DB (blobFieldDecoder) +import Simplex.Messaging.Agent.Store.DB (fromTextField_) import Simplex.Messaging.Encoding.String import Simplex.Messaging.Util ((<$?>)) @@ -20,34 +20,33 @@ data GroupMemberRole | GROwner -- + delete and change group information, add/remove/change roles for Owners deriving (Eq, Show, Ord) -instance FromField GroupMemberRole where fromField = blobFieldDecoder strDecode +instance FromField GroupMemberRole where fromField = fromTextField_ textDecode -instance ToField GroupMemberRole where toField = toField . strEncode +instance ToField GroupMemberRole where toField = toField . textEncode -instance StrEncoding GroupMemberRole where - strEncode = \case +instance TextEncoding GroupMemberRole where + textEncode = \case GROwner -> "owner" GRAdmin -> "admin" GRModerator -> "moderator" GRMember -> "member" GRAuthor -> "author" GRObserver -> "observer" - strDecode = \case - "owner" -> Right GROwner - "admin" -> Right GRAdmin - "moderator" -> Right GRModerator - "member" -> Right GRMember - "author" -> Right GRAuthor - "observer" -> Right GRObserver - r -> Left $ "bad GroupMemberRole " <> B.unpack r - strP = strDecode <$?> A.takeByteString + textDecode = \case + "owner" -> Just GROwner + "admin" -> Just GRAdmin + "moderator" -> Just GRModerator + "member" -> Just GRMember + "author" -> Just GRAuthor + "observer" -> Just GRObserver + r -> Nothing instance FromJSON GroupMemberRole where - parseJSON = strParseJSON "GroupMemberRole" + parseJSON = textParseJSON "GroupMemberRole" instance ToJSON GroupMemberRole where - toJSON = strToJSON - toEncoding = strToJEncoding + toJSON = textToJSON + toEncoding = textToEncoding data GroupAcceptance = GAAccepted | GAPendingApproval | GAPendingReview deriving (Eq, Show) diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs index a8526e357f..b515123928 100644 --- a/src/Simplex/Chat/View.hs +++ b/src/Simplex/Chat/View.hs @@ -1231,7 +1231,7 @@ viewConnectedToGroupMember g@GroupInfo {groupId} m@GroupMember {groupMemberId, m viewReceivedGroupInvitation :: GroupInfo -> Contact -> GroupMemberRole -> [StyledString] viewReceivedGroupInvitation g c role = - ttyFullGroup g <> ": " <> ttyContact' c <> " invites you to join the group as " <> plain (strEncode role) + ttyFullGroup g <> ": " <> ttyContact' c <> " invites you to join the group as " <> showRole role : case incognitoMembershipProfile g of Just mp -> ["use " <> highlight ("/j " <> viewGroupName g) <> " to join incognito as " <> incognitoProfile' (fromLocalProfile mp)] Nothing -> ["use " <> highlight ("/j " <> viewGroupName g) <> " to accept"] @@ -1270,15 +1270,15 @@ viewMembersBlockedForAllUser g members blocked = case members of mems' -> [ttyGroup' g <> ": you " <> (if blocked then "blocked" else "unblocked") <> " " <> sShow (length mems') <> " members"] showRole :: GroupMemberRole -> StyledString -showRole = plain . strEncode +showRole = plain . textEncode viewGroupMembers :: Group -> [StyledString] viewGroupMembers (Group GroupInfo {membership} members) = map groupMember . filter (not . removedOrLeft) $ membership : members where removedOrLeft m = let s = memberStatus m in s == GSMemRejected || s == GSMemRemoved || s == GSMemLeft - groupMember m = memIncognito m <> ttyFullMember m <> ": " <> plain (intercalate ", " $ [role m] <> category m <> status m <> muted m) - role :: GroupMember -> String - role GroupMember {memberRole} = B.unpack $ strEncode memberRole + groupMember m = memIncognito m <> ttyFullMember m <> ": " <> plain (T.intercalate ", " $ [role m] <> category m <> status m <> muted m) + role :: GroupMember -> Text + role GroupMember {memberRole} = textEncode memberRole category m = case memberCategory m of GCUserMember -> ["you"] GCInviteeMember -> ["invited"] @@ -2455,8 +2455,8 @@ viewChatError isCmd logLevel testView = \case CEGroupUserRole g role -> (: []) . (ttyGroup' g <>) $ case role of GRAuthor -> ": you don't have permission to send messages" - _ -> ": you have insufficient permissions for this action, the required role is " <> plain (strEncode role) - CEGroupMemberInitialRole g role -> [ttyGroup' g <> ": initial role for group member cannot be " <> plain (strEncode role) <> ", use member or observer"] + _ -> ": you have insufficient permissions for this action, the required role is " <> showRole role + CEGroupMemberInitialRole g role -> [ttyGroup' g <> ": initial role for group member cannot be " <> showRole role <> ", use member or observer"] CEContactIncognitoCantInvite -> ["you're using your main profile for this group - prohibited to invite contacts to whom you are connected incognito"] CEGroupIncognitoCantInvite -> ["you are using an incognito profile for this group - prohibited to invite contacts"] CEGroupContactRole c -> ["contact " <> ttyContact c <> " has insufficient permissions for this group action"] diff --git a/tests/ChatTests/Groups.hs b/tests/ChatTests/Groups.hs index 166528e69c..121d5a92f8 100644 --- a/tests/ChatTests/Groups.hs +++ b/tests/ChatTests/Groups.hs @@ -8389,7 +8389,7 @@ testChannelsRelayDeliver = createChannel5 :: TestCC -> TestCC -> TestCC -> TestCC -> TestCC -> GroupMemberRole -> IO () createChannel5 alice bob cath dan eve mRole = do createGroup2 "team" alice bob - bob ##> ("/create link #team " <> B.unpack (strEncode mRole)) + bob ##> ("/create link #team " <> T.unpack (textEncode mRole)) gLink <- getGroupLink bob "team" mRole True cath ##> ("/c " <> gLink) cath <## "connection request sent!" diff --git a/tests/ChatTests/Utils.hs b/tests/ChatTests/Utils.hs index 0e99b26bc5..dded39e692 100644 --- a/tests/ChatTests/Utils.hs +++ b/tests/ChatTests/Utils.hs @@ -593,7 +593,7 @@ getGroupLink_ cc gName mRole created = do cc <## "" link <- getTermLine cc cc <## "" - cc <## ("Anybody can connect to you and join group as " <> B.unpack (strEncode mRole) <> " with: /c ") + cc <## ("Anybody can connect to you and join group as " <> T.unpack (textEncode mRole) <> " with: /c ") cc <## ("to show it again: /show link #" <> gName) cc <## ("to delete it: /delete link #" <> gName <> " (joined members will remain connected to you)") pure link @@ -809,12 +809,12 @@ fullAddMember :: HasCallStack => String -> String -> TestCC -> TestCC -> GroupMe fullAddMember gName fullName inviting invitee role = do name1 <- userName inviting memName <- userName invitee - inviting ##> ("/a " <> gName <> " " <> memName <> " " <> B.unpack (strEncode role)) + inviting ##> ("/a " <> gName <> " " <> memName <> " " <> T.unpack (textEncode role)) let fullName' = if null fullName || fullName == gName then "" else " (" <> fullName <> ")" concurrentlyN_ [ inviting <## ("invitation to join the group #" <> gName <> " sent to " <> memName), do - invitee <## ("#" <> gName <> fullName' <> ": " <> name1 <> " invites you to join the group as " <> B.unpack (strEncode role)) + invitee <## ("#" <> gName <> fullName' <> ": " <> name1 <> " invites you to join the group as " <> T.unpack (textEncode role)) invitee <## ("use /j " <> gName <> " to accept") ] diff --git a/tests/SchemaDump.hs b/tests/SchemaDump.hs index 5646031cc2..2c4ba05ce6 100644 --- a/tests/SchemaDump.hs +++ b/tests/SchemaDump.hs @@ -17,7 +17,7 @@ import Data.Maybe (fromJust, isJust) import Data.Text (Text) import qualified Data.Text as T import qualified Data.Text.IO as T -import Database.SQLite.Simple (Query (..)) +import Database.SQLite.Simple (Only (..), Query (..)) import Simplex.Chat.Options.SQLite (chatDBFunctions) import Simplex.Chat.Store (createChatStore) import qualified Simplex.Chat.Store as Store @@ -59,6 +59,7 @@ schemaDumpTest = do it "verify and overwrite schema dump" testVerifySchemaDump it "verify .lint fkey-indexes" testVerifyLintFKeyIndexes it "verify schema down migrations" testSchemaMigrations + it "verify strict tables" testVerifyStrict testVerifySchemaDump :: IO () testVerifySchemaDump = withTmpFiles $ do @@ -101,6 +102,12 @@ testSchemaMigrations = withTmpFiles $ do schema''' <- getSchema testDB testSchema schema''' `shouldBe` schema' +testVerifyStrict :: IO () +testVerifyStrict = do + Right st <- createDBStore (DBOpts testDB chatDBFunctions "" False True TQOff) Store.migrations (MigrationConfig MCConsole Nothing) + withConnection st (`DB.query_` "SELECT name FROM sqlite_master WHERE type = 'table' AND name != 'sqlite_sequence' AND sql NOT LIKE '% STRICT'") + `shouldReturn` ([] :: [Only Text]) + skipComparisonForUpMigrations :: [String] skipComparisonForUpMigrations = [ -- schema doesn't change From 2251da970e04a229afd5df712b2ef00af1d480b7 Mon Sep 17 00:00:00 2001 From: Evgeny Date: Mon, 5 Jan 2026 22:10:51 +0000 Subject: [PATCH 04/73] core: reset schema after changes (#6545) * core: reset schema after changes * update simplexmq --- cabal.project | 2 +- scripts/nix/sha256map.nix | 2 +- .../Chat/Store/SQLite/Migrations/M20240226_users_restrict.hs | 4 ++-- .../SQLite/Migrations/M20241023_chat_item_autoincrement_id.hs | 4 ++-- .../M20250702_contact_requests_remove_cascade_delete.hs | 4 ++-- .../Store/SQLite/Migrations/M20251017_chat_tags_cascade.hs | 4 ++-- .../Chat/Store/SQLite/Migrations/M20251230_strict_tables.hs | 4 ++-- 7 files changed, 12 insertions(+), 12 deletions(-) diff --git a/cabal.project b/cabal.project index e5d7464ece..63e85073a1 100644 --- a/cabal.project +++ b/cabal.project @@ -12,7 +12,7 @@ constraints: zip +disable-bzip2 +disable-zstd source-repository-package type: git location: https://github.com/simplex-chat/simplexmq.git - tag: a7b43b1a3e204759d4b7ad60928fa897b1600654 + tag: c4b687ba644d8f0581a9f4317b6211c493a8d685 source-repository-package type: git diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index 16454f63d1..fdf8735197 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,5 +1,5 @@ { - "https://github.com/simplex-chat/simplexmq.git"."a7b43b1a3e204759d4b7ad60928fa897b1600654" = "169vjn5gyw42cmak6kwyl27zm57il43khnlj40zjwjw7cldkzdzi"; + "https://github.com/simplex-chat/simplexmq.git"."c4b687ba644d8f0581a9f4317b6211c493a8d685" = "0s6wnmxjjr3fgfayyn0rdgwkqsg4z6da6ha0sq78mavvplwhg21m"; "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/Store/SQLite/Migrations/M20240226_users_restrict.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20240226_users_restrict.hs index eb1bc2bfea..150cccd900 100644 --- a/src/Simplex/Chat/Store/SQLite/Migrations/M20240226_users_restrict.hs +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20240226_users_restrict.hs @@ -14,7 +14,7 @@ UPDATE sqlite_master SET sql = replace(sql, 'ON DELETE CASCADE', 'ON DELETE RESTRICT') WHERE name = 'users' AND type = 'table'; -PRAGMA writable_schema=0; +PRAGMA writable_schema=RESET; |] down_m20240226_users_restrict :: Query @@ -26,5 +26,5 @@ UPDATE sqlite_master SET sql = replace(sql, 'ON DELETE RESTRICT', 'ON DELETE CASCADE') WHERE name = 'users' AND type = 'table'; -PRAGMA writable_schema=0; +PRAGMA writable_schema=RESET; |] diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/M20241023_chat_item_autoincrement_id.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20241023_chat_item_autoincrement_id.hs index 03b5c40ed3..2764b60850 100644 --- a/src/Simplex/Chat/Store/SQLite/Migrations/M20241023_chat_item_autoincrement_id.hs +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20241023_chat_item_autoincrement_id.hs @@ -16,7 +16,7 @@ PRAGMA writable_schema=1; UPDATE sqlite_master SET sql = replace(sql, 'INTEGER PRIMARY KEY', 'INTEGER PRIMARY KEY AUTOINCREMENT') WHERE name = 'chat_items' AND type = 'table'; -PRAGMA writable_schema=0; +PRAGMA writable_schema=RESET; |] down_m20241023_chat_item_autoincrement_id :: Query @@ -30,5 +30,5 @@ UPDATE sqlite_master SET sql = replace(sql, 'INTEGER PRIMARY KEY AUTOINCREMENT', 'INTEGER PRIMARY KEY') WHERE name = 'chat_items' AND type = 'table'; -PRAGMA writable_schema=0; +PRAGMA writable_schema=RESET; |] diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/M20250702_contact_requests_remove_cascade_delete.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20250702_contact_requests_remove_cascade_delete.hs index c758a962cd..5a4d91c826 100644 --- a/src/Simplex/Chat/Store/SQLite/Migrations/M20250702_contact_requests_remove_cascade_delete.hs +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20250702_contact_requests_remove_cascade_delete.hs @@ -22,7 +22,7 @@ SET sql = replace( ) WHERE name = 'contact_requests' AND type = 'table'; -PRAGMA writable_schema=0; +PRAGMA writable_schema=RESET; |] down_m20250702_contact_requests_remove_cascade_delete :: Query @@ -42,5 +42,5 @@ SET sql = replace( ) WHERE name = 'contact_requests' AND type = 'table'; -PRAGMA writable_schema=0; +PRAGMA writable_schema=RESET; |] diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/M20251017_chat_tags_cascade.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20251017_chat_tags_cascade.hs index 1f82831404..bda307307d 100644 --- a/src/Simplex/Chat/Store/SQLite/Migrations/M20251017_chat_tags_cascade.hs +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20251017_chat_tags_cascade.hs @@ -14,7 +14,7 @@ UPDATE sqlite_master SET sql = replace(sql, 'user_id INTEGER REFERENCES users', 'user_id INTEGER REFERENCES users ON DELETE CASCADE') WHERE name = 'chat_tags' AND type = 'table'; -PRAGMA writable_schema=0; +PRAGMA writable_schema=RESET; |] down_m20251017_chat_tags_cascade :: Query @@ -26,5 +26,5 @@ UPDATE sqlite_master SET sql = replace(sql, 'user_id INTEGER REFERENCES users ON DELETE CASCADE', 'user_id INTEGER REFERENCES users') WHERE name = 'chat_tags' AND type = 'table'; -PRAGMA writable_schema=0; +PRAGMA writable_schema=RESET; |] diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/M20251230_strict_tables.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20251230_strict_tables.hs index be92da8f5e..760b5f8073 100644 --- a/src/Simplex/Chat/Store/SQLite/Migrations/M20251230_strict_tables.hs +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20251230_strict_tables.hs @@ -34,7 +34,7 @@ SET sql = CASE END WHERE type = 'table' AND name != 'sqlite_sequence'; -PRAGMA writable_schema=0; +PRAGMA writable_schema=RESET; |] down_m20251230_strict_tables :: Query @@ -58,7 +58,7 @@ UPDATE sqlite_master SET sql = replace(sql, 'local_alias TEXT DEFAULT', 'local_alias DEFAULT') WHERE type = 'table' AND name = 'connections'; -PRAGMA writable_schema=0; +PRAGMA writable_schema=RESET; UPDATE group_members SET member_role = CAST(member_role as BLOB), From d6eebd52fc0ddfdb91c2411bcb649b34bf66a12e Mon Sep 17 00:00:00 2001 From: sh <37271604+shumvgolove@users.noreply.github.com> Date: Wed, 7 Jan 2026 13:20:20 +0000 Subject: [PATCH 05/73] desktop: rename library to libsimplex (#6528) Co-authored-by: Evgeny Poberezkin --- .../src/commonMain/cpp/desktop/CMakeLists.txt | 3 +-- scripts/desktop/build-lib-linux.sh | 15 ++++++++------- scripts/desktop/build-lib-mac.sh | 9 +++++---- 3 files changed, 14 insertions(+), 13 deletions(-) diff --git a/apps/multiplatform/common/src/commonMain/cpp/desktop/CMakeLists.txt b/apps/multiplatform/common/src/commonMain/cpp/desktop/CMakeLists.txt index 059e5af426..1ebfdce6b1 100644 --- a/apps/multiplatform/common/src/commonMain/cpp/desktop/CMakeLists.txt +++ b/apps/multiplatform/common/src/commonMain/cpp/desktop/CMakeLists.txt @@ -54,11 +54,10 @@ add_library( # Sets the name of the library. simplex-api.c) add_library( simplex SHARED IMPORTED ) +FILE(GLOB SIMPLEXLIB ${CMAKE_SOURCE_DIR}/libs/${OS_LIB_PATH}-${OS_LIB_ARCH}/libsimplex.${OS_LIB_EXT}) if(WIN32) - FILE(GLOB SIMPLEXLIB ${CMAKE_SOURCE_DIR}/libs/${OS_LIB_PATH}-${OS_LIB_ARCH}/lib*simplex*.${OS_LIB_EXT}) set_target_properties( simplex PROPERTIES IMPORTED_IMPLIB ${SIMPLEXLIB}) else() - FILE(GLOB SIMPLEXLIB ${CMAKE_SOURCE_DIR}/libs/${OS_LIB_PATH}-${OS_LIB_ARCH}/lib*simplex-chat*.${OS_LIB_EXT}) set_target_properties( simplex PROPERTIES IMPORTED_LOCATION ${SIMPLEXLIB}) endif() diff --git a/scripts/desktop/build-lib-linux.sh b/scripts/desktop/build-lib-linux.sh index 6b197a0b8b..a9deb28d9a 100755 --- a/scripts/desktop/build-lib-linux.sh +++ b/scripts/desktop/build-lib-linux.sh @@ -24,18 +24,19 @@ exports=( $(sed 's/foreign export ccall "chat_migrate_init_key"//' src/Simplex/C for elem in "${exports[@]}"; do count=$(grep -R "$elem$" libsimplex.dll.def | wc -l); if [ $count -ne 1 ]; then echo Wrong exports in libsimplex.dll.def. Add \"$elem\" to that file; exit 1; fi ; done for elem in "${exports[@]}"; do count=$(grep -R "\"$elem\"" flake.nix | wc -l); if [ $count -ne 2 ]; then echo Wrong exports in flake.nix. Add \"$elem\" in two places of the file; exit 1; fi ; done -rm -rf $BUILD_DIR -cabal build lib:simplex-chat --ghc-options='-optl-Wl,-rpath,$ORIGIN -flink-rts -threaded' --constraint 'simplexmq +client_library' --constraint 'simplex-chat +client_library' +#rm -rf $BUILD_DIR +cabal build lib:simplex-chat --ghc-options='-optl-Wl,-rpath,$ORIGIN -optl-Wl,-soname,libsimplex.so -flink-rts -threaded' --constraint 'simplexmq +client_library' --constraint 'simplex-chat +client_library' cd $BUILD_DIR/build -#patchelf --add-needed libHSrts_thr-ghc${GHC_VERSION}.so libHSsimplex-chat-*-inplace-ghc${GHC_VERSION}.so -#patchelf --add-rpath '$ORIGIN' libHSsimplex-chat-*-inplace-ghc${GHC_VERSION}.so +mv libHSsimplex-chat-*-inplace-ghc${GHC_VERSION}.so libsimplex.so 2> /dev/null || true +#patchelf --add-needed libHSrts_thr-ghc${GHC_VERSION}.so libsimplex.so +#patchelf --add-rpath '$ORIGIN' libsimplex.so # GitHub's Ubuntu 20.04 runner started to set libffi.so.7 as a dependency while Ubuntu 20.04 on user's devices may not have it # but libffi.so.8 is shipped as an external library with other libs -patchelf --replace-needed "libffi.so.7" "libffi.so.8" libHSsimplex-chat-*-inplace-ghc${GHC_VERSION}.so +patchelf --replace-needed "libffi.so.7" "libffi.so.8" libsimplex.so mkdir deps 2> /dev/null || true -ldd libHSsimplex-chat-*-inplace-ghc${GHC_VERSION}.so | grep "ghc" | cut -d' ' -f 3 | xargs -I {} cp {} ./deps/ +ldd libsimplex.so | grep "ghc" | cut -d' ' -f 3 | xargs -I {} cp {} ./deps/ cd - @@ -44,7 +45,7 @@ rm -rf apps/multiplatform/desktop/build/cmake mkdir -p apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/ cp -r $BUILD_DIR/build/deps/* apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/ -cp $BUILD_DIR/build/libHSsimplex-chat-*-inplace-ghc${GHC_VERSION}.so apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/ +cp $BUILD_DIR/build/libsimplex.so apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/ scripts/desktop/prepare-vlc-linux.sh links_dir=apps/multiplatform/build/links diff --git a/scripts/desktop/build-lib-mac.sh b/scripts/desktop/build-lib-mac.sh index 6e3415bd80..782610b302 100755 --- a/scripts/desktop/build-lib-mac.sh +++ b/scripts/desktop/build-lib-mac.sh @@ -15,7 +15,7 @@ else fi LIB_EXT=dylib -LIB=libHSsimplex-chat-*-inplace-ghc*.$LIB_EXT +LIB=libsimplex.$LIB_EXT GHC_LIBS_DIR=$(ghc --print-libdir) BUILD_DIR=dist-newstyle/build/$ARCH-*/ghc-*/simplex-chat-* @@ -28,13 +28,14 @@ rm -rf $BUILD_DIR if [[ "$DATABASE_BACKEND" == "postgres" ]]; then echo "Building with postgres backend..." - cabal build lib:simplex-chat lib:simplex-chat --ghc-options="-optl-Wl,-rpath,@loader_path -optl-Wl,-L$GHC_LIBS_DIR/$ARCH-osx-ghc-$GHC_VERSION -optl-lHSrts_thr-ghc$GHC_VERSION -optl-lffi" --constraint 'simplexmq +client_library +client_postgres' --constraint 'simplex-chat +client_library +client_postgres' + cabal build lib:simplex-chat lib:simplex-chat --ghc-options="-optl-Wl,-rpath,@loader_path -optl-Wl,-install_name,@rpath/$LIB -optl-Wl,-L$GHC_LIBS_DIR/$ARCH-osx-ghc-$GHC_VERSION -optl-lHSrts_thr-ghc$GHC_VERSION -optl-lffi" --constraint 'simplexmq +client_library +client_postgres' --constraint 'simplex-chat +client_library +client_postgres' else echo "Building with sqlite backend..." - cabal build lib:simplex-chat lib:simplex-chat --ghc-options="-optl-Wl,-rpath,@loader_path -optl-Wl,-L$GHC_LIBS_DIR/$ARCH-osx-ghc-$GHC_VERSION -optl-lHSrts_thr-ghc$GHC_VERSION -optl-lffi" --constraint 'simplexmq +client_library' --constraint 'simplex-chat +client_library' + cabal build lib:simplex-chat lib:simplex-chat --ghc-options="-optl-Wl,-rpath,@loader_path -optl-Wl,-install_name,@rpath/$LIB -optl-Wl,-L$GHC_LIBS_DIR/$ARCH-osx-ghc-$GHC_VERSION -optl-lHSrts_thr-ghc$GHC_VERSION -optl-lffi" --constraint 'simplexmq +client_library' --constraint 'simplex-chat +client_library' fi cd $BUILD_DIR/build +mv libHSsimplex-chat-*-inplace-ghc*.$LIB_EXT libsimplex.dylib 2> /dev/null || true mkdir deps 2> /dev/null || true # It's not included by default for some reason. Compiled lib tries to find system one but it's not always available @@ -103,7 +104,7 @@ rm -rf apps/multiplatform/desktop/build/cmake mkdir -p apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/ cp -r $BUILD_DIR/build/deps/* apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/ -cp $BUILD_DIR/build/libHSsimplex-chat-*-inplace-ghc*.$LIB_EXT apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/ +cp $BUILD_DIR/build/$LIB apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/ cd apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/ From 3596c37275fcb2bd6892aefde0e6ae5a442d2f4b Mon Sep 17 00:00:00 2001 From: Evgeny Date: Thu, 8 Jan 2026 13:43:37 +0000 Subject: [PATCH 06/73] core: improve database concurrency (#6541) * core: improve database concurrency * tests: prints on timeouts (#6546) * update simplexmq * fix test * update simplexmq --------- Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> --- cabal.project | 2 +- scripts/nix/sha256map.nix | 2 +- src/Simplex/Chat/Library/Internal.hs | 18 +++--- src/Simplex/Chat/Library/Subscriber.hs | 81 ++++++++++++++------------ src/Simplex/Chat/Store/Groups.hs | 12 ++-- src/Simplex/Chat/Store/Messages.hs | 46 +++++++-------- tests/Bots/DirectoryTests.hs | 12 ++-- tests/ChatClient.hs | 16 +++-- tests/ChatTests/Groups.hs | 6 -- tests/ChatTests/Utils.hs | 60 +++++++++++-------- 10 files changed, 139 insertions(+), 116 deletions(-) diff --git a/cabal.project b/cabal.project index 63e85073a1..0756146fe0 100644 --- a/cabal.project +++ b/cabal.project @@ -12,7 +12,7 @@ constraints: zip +disable-bzip2 +disable-zstd source-repository-package type: git location: https://github.com/simplex-chat/simplexmq.git - tag: c4b687ba644d8f0581a9f4317b6211c493a8d685 + tag: 6aadcf1f3fc19cbc0c8be457556fbaaffb0bfc46 source-repository-package type: git diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index fdf8735197..8f77a6505e 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,5 +1,5 @@ { - "https://github.com/simplex-chat/simplexmq.git"."c4b687ba644d8f0581a9f4317b6211c493a8d685" = "0s6wnmxjjr3fgfayyn0rdgwkqsg4z6da6ha0sq78mavvplwhg21m"; + "https://github.com/simplex-chat/simplexmq.git"."6aadcf1f3fc19cbc0c8be457556fbaaffb0bfc46" = "1qlm542jnik48zid3zy7iys7ybjmlmj3mjhc5aplfk410a5qsb93"; "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/Library/Internal.hs b/src/Simplex/Chat/Library/Internal.hs index 5b445cf460..8e1e6705df 100644 --- a/src/Simplex/Chat/Library/Internal.hs +++ b/src/Simplex/Chat/Library/Internal.hs @@ -707,8 +707,8 @@ acceptFileReceive user@User {userId} RcvFileTransfer {fileId, xftpRcvFile, fileI if | inline -> do -- accepting inline - ci <- withStore $ \db -> acceptRcvInlineFT db vr user fileId filePath - sharedMsgId <- withStore $ \db -> getSharedMsgIdByFileId db userId fileId + (ci, sharedMsgId) <- withStore $ \db -> + liftM2 (,) (acceptRcvInlineFT db vr user fileId filePath) (getSharedMsgIdByFileId db userId fileId) send $ XFileAcptInv sharedMsgId Nothing fName pure ci | fileInline == Just IFMSent -> throwChatError $ CEFileAlreadyReceiving fName @@ -925,9 +925,11 @@ acceptGroupJoinRequestAsync incognitoProfile = do gVar <- asks random let initialStatus = acceptanceToStatus (memberAdmission groupProfile) gAccepted - (groupMemberId, memberId) <- withStore $ \db -> - createJoiningMember db gVar user gInfo cReqChatVRange cReqProfile cReqXContactId_ welcomeMsgId_ gLinkMemRole initialStatus - currentMemCount <- withStore' $ \db -> getGroupCurrentMembersCount db user gInfo + ((groupMemberId, memberId), currentMemCount) <- withStore $ \db -> + liftM2 + (,) + (createJoiningMember db gVar user gInfo cReqChatVRange cReqProfile cReqXContactId_ welcomeMsgId_ gLinkMemRole initialStatus) + (liftIO $ getGroupCurrentMembersCount db user gInfo) let Profile {displayName} = userProfileInGroup user gInfo (fromIncognitoProfile <$> incognitoProfile) GroupMember {memberRole = userRole, memberId = userMemberId} = membership msg = @@ -1041,15 +1043,13 @@ introduceToModerators vr user gInfo@GroupInfo {groupId} m@GroupMember {memberRol introduceToAll :: VersionRangeChat -> User -> GroupInfo -> GroupMember -> CM () introduceToAll vr user gInfo m = do - members <- withStore' $ \db -> getGroupMembers db vr user gInfo - vector <- withStore (`getMemberRelationsVector` m) + (members, vector) <- withStore $ \db -> liftM2 (,) (liftIO $ getGroupMembers db vr user gInfo) (getMemberRelationsVector db m) let recipients = filter (shouldIntroduce m vector) members introduceMember user gInfo m recipients Nothing introduceToRemaining :: VersionRangeChat -> User -> GroupInfo -> GroupMember -> CM () introduceToRemaining vr user gInfo m = do - members <- withStore' $ \db -> getGroupMembers db vr user gInfo - vector <- withStore (`getMemberRelationsVector` m) + (members, vector) <- withStore $ \db -> liftM2 (,) (liftIO $ getGroupMembers db vr user gInfo) (getMemberRelationsVector db m) let recipients = filter (shouldIntroduce m vector) members introduceMember user gInfo m recipients Nothing diff --git a/src/Simplex/Chat/Library/Subscriber.hs b/src/Simplex/Chat/Library/Subscriber.hs index 625e879607..e10bf2a081 100644 --- a/src/Simplex/Chat/Library/Subscriber.hs +++ b/src/Simplex/Chat/Library/Subscriber.hs @@ -691,9 +691,10 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = -- TODO REMOVE LEGACY vvv -- [async agent commands] group link auto-accept continuation on receiving INV CFCreateConnGrpInv -> do - ct <- withStore $ \db -> getContactViaMember db vr user m - withStore' $ \db -> setNewContactMemberConnRequest db user m cReq - groupLinkId <- withStore' $ \db -> getGroupLinkId db user gInfo + (ct, groupLinkId) <- withStore $ \db -> do + ct <- getContactViaMember db vr user m + liftIO $ setNewContactMemberConnRequest db user m cReq + liftIO $ (ct,) <$> getGroupLinkId db user gInfo sendGrpInvitation ct m groupLinkId toView $ CEvtSentGroupInvitation user gInfo ct m where @@ -1814,8 +1815,9 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = ts@(_, ft_) = msgContentTexts mc live = fromMaybe False live_ updateRcvChatItem = do - cci <- withStore $ \db -> getGroupChatItemBySharedMsgId db user gInfo groupMemberId sharedMsgId - scopeInfo <- withStore $ \db -> getGroupChatScopeInfoForItem db vr user gInfo (cChatItemId cci) + (cci, scopeInfo) <- withStore $ \db -> do + cci <- getGroupChatItemBySharedMsgId db user gInfo groupMemberId sharedMsgId + (cci,) <$> getGroupChatScopeInfoForItem db vr user gInfo (cChatItemId cci) case cci of CChatItem SMDRcv ci@ChatItem {chatDir = CIGroupRcv m', meta = CIMeta {itemLive}, content = CIRcvMsgContent oldMC} -> if sameMemberId memberId m' @@ -1948,8 +1950,9 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = xFileCancel :: Contact -> SharedMsgId -> CM () xFileCancel Contact {contactId} sharedMsgId = do - fileId <- withStore $ \db -> getFileIdBySharedMsgId db userId contactId sharedMsgId - ft <- withStore (\db -> getRcvFileTransfer db user fileId) + (fileId, ft) <- withStore $ \db -> do + fileId <- getFileIdBySharedMsgId db userId contactId sharedMsgId + (fileId,) <$> getRcvFileTransfer db user fileId unless (rcvFileCompleteOrCancelled ft) $ do cancelRcvFileTransfer user ft ci <- withStore $ \db -> getChatItemByFileId db vr user fileId @@ -1957,8 +1960,9 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = xFileAcptInv :: Contact -> SharedMsgId -> Maybe ConnReqInvitation -> String -> CM () xFileAcptInv ct sharedMsgId fileConnReq_ fName = do - fileId <- withStore $ \db -> getDirectFileIdBySharedMsgId db user ct sharedMsgId - (AChatItem _ _ _ ci) <- withStore $ \db -> getChatItemByFileId db vr user fileId + (fileId, AChatItem _ _ _ ci) <- withStore $ \db -> do + fileId <- getDirectFileIdBySharedMsgId db user ct sharedMsgId + (fileId,) <$> getChatItemByFileId db vr user fileId assertSMPAcceptNotProhibited ci ft@FileTransferMeta {fileName, fileSize, fileInline, cancelled} <- withStore (\db -> getFileTransferMeta db user fileId) -- [async agent commands] no continuation needed, but command should be asynchronous for stability @@ -2033,8 +2037,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = xFileCancelGroup g@GroupInfo {groupId} GroupMember {memberId} sharedMsgId = do (fileId, aci) <- withStore $ \db -> do fileId <- getGroupFileIdBySharedMsgId db userId groupId sharedMsgId - aci <- getChatItemByFileId db vr user fileId - pure (fileId, aci) + (fileId,) <$> getChatItemByFileId db vr user fileId case aci of AChatItem SCTGroup SMDRcv (GroupChat _g scopeInfo) ChatItem {chatDir = CIGroupRcv m} -> do if sameMemberId memberId m @@ -2051,8 +2054,9 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = xFileAcptInvGroup :: GroupInfo -> GroupMember -> SharedMsgId -> Maybe ConnReqInvitation -> String -> CM () xFileAcptInvGroup GroupInfo {groupId} m@GroupMember {activeConn} sharedMsgId fileConnReq_ fName = do - fileId <- withStore $ \db -> getGroupFileIdBySharedMsgId db userId groupId sharedMsgId - (AChatItem _ _ _ ci) <- withStore $ \db -> getChatItemByFileId db vr user fileId + (fileId, AChatItem _ _ _ ci) <- withStore $ \db -> do + fileId <- getGroupFileIdBySharedMsgId db userId groupId sharedMsgId + (fileId,) <$> getChatItemByFileId db vr user fileId assertSMPAcceptNotProhibited ci -- TODO check that it's not already accepted ft@FileTransferMeta {fileName, fileSize, fileInline, cancelled} <- withStore (\db -> getFileTransferMeta db user fileId) @@ -2123,8 +2127,9 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = xDirectDel c msg msgMeta = if directOrUsed c then do - ct' <- withStore' $ \db -> updateContactStatus db user c CSDeleted - contactConns <- withStore' $ \db -> getContactConnections db vr userId ct' + (ct', contactConns) <- withStore' $ \db -> do + ct' <- updateContactStatus db user c CSDeleted + (ct',) <$> getContactConnections db vr userId ct' deleteAgentConnectionsAsync $ map aConnId contactConns forM_ contactConns $ \conn -> withStore' $ \db -> updateConnectionStatus db conn ConnDeleted activeConn' <- forM (contactConn ct') $ \conn -> pure conn {connStatus = ConnDeleted} @@ -2496,15 +2501,16 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = associateMemberWithContact :: Contact -> GroupMember -> CM Contact associateMemberWithContact c1 m2@GroupMember {groupId} = do - withStore' $ \db -> associateMemberWithContactRecord db user c1 m2 - g <- withStore $ \db -> getGroupInfo db vr user groupId + g <- withStore $ \db -> do + liftIO $ associateMemberWithContactRecord db user c1 m2 + getGroupInfo db vr user groupId toView $ CEvtContactAndMemberAssociated user c1 g m2 c1 pure c1 associateContactWithMember :: GroupMember -> Contact -> CM Contact associateContactWithMember m1@GroupMember {groupId} c2 = do - c2' <- withStore $ \db -> associateContactWithMemberRecord db vr user m1 c2 - g <- withStore $ \db -> getGroupInfo db vr user groupId + (c2', g) <- withStore $ \db -> + liftM2 (,) (associateContactWithMemberRecord db vr user m1 c2) (getGroupInfo db vr user groupId) toView $ CEvtContactAndMemberAssociated user c2 g m1 c2' pure c2' @@ -2622,19 +2628,21 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = xGrpMemFwd gInfo@GroupInfo {membership, chatSettings} m memInfo@(MemberInfo memId memRole memChatVRange _) IntroInvitation {groupConnReq, directConnReq} = do let GroupMember {memberId = membershipMemId} = membership checkHostRole m memRole - toMember <- - withStore' (\db -> runExceptT $ getGroupMemberByMemberId db vr user gInfo memId) >>= \case + toMember <- withStore $ \db -> do + toMember <- getGroupMemberByMemberId db vr user gInfo memId -- TODO if the missed messages are correctly sent as soon as there is connection before anything else is sent -- the situation when member does not exist is an error -- member receiving x.grp.mem.fwd should have also received x.grp.mem.new prior to that. -- For now, this branch compensates for the lack of delayed message delivery. - Left _ -> withStore $ \db -> createNewGroupMember db user gInfo m memInfo GCPostMember GSMemAnnounced - Right m' -> pure m' - -- TODO [knocking] separate pending statuses from GroupMemberStatus? - -- TODO add GSMemIntroInvitedPending, GSMemConnectedPending, etc.? - -- TODO keep as is? (GSMemIntroInvited has no purpose) - let newMemberStatus = if memberPending toMember then memberStatus toMember else GSMemIntroInvited - withStore' $ \db -> updateGroupMemberStatus db userId toMember newMemberStatus + `catchError` \case + SEGroupMemberNotFoundByMemberId _ -> createNewGroupMember db user gInfo m memInfo GCPostMember GSMemAnnounced + e -> throwError e + -- TODO [knocking] separate pending statuses from GroupMemberStatus? + -- TODO add GSMemIntroInvitedPending, GSMemConnectedPending, etc.? + -- TODO keep as is? (GSMemIntroInvited has no purpose) + let newMemberStatus = if memberPending toMember then memberStatus toMember else GSMemIntroInvited + liftIO $ updateGroupMemberStatus db userId toMember newMemberStatus + pure toMember subMode <- chatReadVar subscriptionMode -- [incognito] send membership incognito profile, create direct connection as incognito let membershipProfile = redactedMemberProfile allowSimplexLinks $ fromLocalProfile $ memberProfile membership @@ -3021,14 +3029,15 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = updateGroupItemsStatus :: GroupInfo -> GroupMember -> Connection -> AgentMsgId -> GroupSndStatus -> Maybe Bool -> CM () updateGroupItemsStatus gInfo@GroupInfo {groupId} GroupMember {groupMemberId} Connection {connId} msgId newMemStatus viaProxy_ = do - items <- withStore' (\db -> getGroupChatItemsByAgentMsgId db user groupId connId msgId) - cis <- catMaybes <$> withStore (\db -> mapM (updateItem db) items) - -- SENT and RCVD events are received for messages that may be batched in single scope, - -- so we can look up scope of first item - scopeInfo <- case cis of - (ci : _) -> withStore $ \db -> getGroupChatScopeInfoForItem db vr user gInfo (chatItemId' ci) - _ -> pure Nothing - let acis = map (gItem scopeInfo) cis + acis <- withStore $ \db -> do + items <- liftIO $ getGroupChatItemsByAgentMsgId db user groupId connId msgId + cis <- catMaybes <$> mapM (updateItem db) items + -- SENT and RCVD events are received for messages that may be batched in single scope, + -- so we can look up scope of first item + scopeInfo <- case cis of + (ci : _) -> getGroupChatScopeInfoForItem db vr user gInfo (chatItemId' ci) + _ -> pure Nothing + pure $ map (gItem scopeInfo) cis unless (null acis) $ toView $ CEvtChatItemsStatusesUpdated user acis where gItem scopeInfo ci = AChatItem SCTGroup SMDSnd (GroupChat gInfo scopeInfo) ci diff --git a/src/Simplex/Chat/Store/Groups.hs b/src/Simplex/Chat/Store/Groups.hs index a0fdb07046..fadc65960b 100644 --- a/src/Simplex/Chat/Store/Groups.hs +++ b/src/Simplex/Chat/Store/Groups.hs @@ -1596,11 +1596,11 @@ setMemberVectorNewRelations db GroupMember {groupMemberId} relations = do v_ <- maybeFirstRow fromOnly $ DB.query db + ( "SELECT member_relations_vector FROM group_members WHERE group_member_id = ?" #if defined(dbPostgres) - "SELECT member_relations_vector FROM group_members WHERE group_member_id = ? FOR UPDATE" -#else - "SELECT member_relations_vector FROM group_members WHERE group_member_id = ?" + <> " FOR UPDATE" #endif + ) (Only groupMemberId) let v' = setNewRelations relations $ fromMaybe B.empty v_ currentTs <- getCurrentTime @@ -1638,11 +1638,11 @@ setMemberVectorRelationConnected db GroupMember {groupMemberId} GroupMember {ind firstRow fromOnly (SEMemberRelationsVectorNotFound groupMemberId) $ DB.query db + ( "SELECT member_relations_vector FROM group_members WHERE group_member_id = ? AND member_relations_vector IS NOT NULL" #if defined(dbPostgres) - "SELECT member_relations_vector FROM group_members WHERE group_member_id = ? AND member_relations_vector IS NOT NULL FOR UPDATE" -#else - "SELECT member_relations_vector FROM group_members WHERE group_member_id = ? AND member_relations_vector IS NOT NULL" + <> " FOR UPDATE" #endif + ) (Only groupMemberId) let v' = setRelationConnected indexInGroup newStatus v currentTs <- liftIO getCurrentTime diff --git a/src/Simplex/Chat/Store/Messages.hs b/src/Simplex/Chat/Store/Messages.hs index 797d0cba11..e3b6911b59 100644 --- a/src/Simplex/Chat/Store/Messages.hs +++ b/src/Simplex/Chat/Store/Messages.hs @@ -52,7 +52,6 @@ module Simplex.Chat.Store.Messages getDirectChatItemLast, getAllChatItems, getAChatItem, - getAChatItemBySharedMsgId, updateDirectChatItem, updateDirectChatItem', addInitialAndNewCIVersions, @@ -1235,13 +1234,17 @@ getDirectChatItemLast db user@User {userId} contactId = do ExceptT . firstRow fromOnly (SEChatItemNotFoundByContactId contactId) $ DB.query db - [sql| - SELECT chat_item_id - FROM chat_items - WHERE user_id = ? AND contact_id = ? - ORDER BY created_at DESC, chat_item_id DESC - LIMIT 1 - |] + ( [sql| + SELECT chat_item_id + FROM chat_items + WHERE user_id = ? AND contact_id = ? + ORDER BY created_at DESC, chat_item_id DESC + LIMIT 1 + |] +#if defined(dbPostgres) + <> " FOR UPDATE" +#endif + ) (userId, contactId) getDirectChatItem db user contactId chatItemId @@ -1560,13 +1563,17 @@ getGroupMemberChatItemLast db user@User {userId} groupId groupMemberId = do ExceptT . firstRow fromOnly (SEChatItemNotFoundByGroupId groupId) $ DB.query db - [sql| - SELECT chat_item_id - FROM chat_items - WHERE user_id = ? AND group_id = ? AND group_member_id = ? - ORDER BY item_ts DESC, chat_item_id DESC - LIMIT 1 - |] + ( [sql| + SELECT chat_item_id + FROM chat_items + WHERE user_id = ? AND group_id = ? AND group_member_id = ? + ORDER BY item_ts DESC, chat_item_id DESC + LIMIT 1 + |] +#if defined(dbPostgres) + <> " FOR UPDATE" +#endif + ) (userId, groupId, groupMemberId) getGroupChatItem db user groupId chatItemId @@ -3243,15 +3250,6 @@ getAChatItem db vr user (ChatRef cType chatId scope) itemId = do _ -> throwError $ SEChatItemNotFound itemId liftIO $ getACIReactions db aci -getAChatItemBySharedMsgId :: ChatTypeQuotable c => DB.Connection -> User -> ChatDirection c 'MDRcv -> SharedMsgId -> ExceptT StoreError IO AChatItem -getAChatItemBySharedMsgId db user cd sharedMsgId = case cd of - CDDirectRcv ct@Contact {contactId} -> do - (CChatItem msgDir ci) <- getDirectChatItemBySharedMsgId db user contactId sharedMsgId - pure $ AChatItem SCTDirect msgDir (DirectChat ct) ci - CDGroupRcv g scopeInfo GroupMember {groupMemberId} -> do - (CChatItem msgDir ci) <- getGroupChatItemBySharedMsgId db user g groupMemberId sharedMsgId - pure $ AChatItem SCTGroup msgDir (GroupChat g scopeInfo) ci - getChatItemVersions :: DB.Connection -> ChatItemId -> IO [ChatItemVersion] getChatItemVersions db itemId = do map toChatItemVersion diff --git a/tests/Bots/DirectoryTests.hs b/tests/Bots/DirectoryTests.hs index 42d59cfa9f..45ae34be0a 100644 --- a/tests/Bots/DirectoryTests.hs +++ b/tests/Bots/DirectoryTests.hs @@ -1167,8 +1167,7 @@ testCapthaScreening ps = bob <## "/'filter 1 off' - disable filter" -- connect with captcha screen _ <- join cath groupLink - cath ##> "/_send #1(_support) text 123" -- sending incorrect captcha - cath <# "#privacy (support) 123" + cath #> "#privacy (support) 123" -- sending incorrect captcha cath <# "#privacy (support) 'SimpleX Directory'!> > cath 123" cath <## " Incorrect text, please try again." captcha <- dropStrPrefix "#privacy (support) 'SimpleX Directory'> " . dropTime <$> getTermLine cath @@ -1220,8 +1219,7 @@ testCapthaScreening ps = cath <## "Send captcha text to join the group privacy." dropStrPrefix "#privacy (support) 'SimpleX Directory'> " . dropTime <$> getTermLine cath sendCaptcha cath captcha = do - cath ##> ("/_send #1(_support) text " <> captcha) - cath <# ("#privacy (support) " <> captcha) + cath #> ("#privacy (support) " <> captcha) cath <# ("#privacy (support) 'SimpleX Directory'!> > cath " <> captcha) cath <## " Correct, you joined the group privacy" cath <## "#privacy: you joined the group" @@ -1411,8 +1409,10 @@ submitGroup u n fn = do groupAccepted :: TestCC -> String -> IO String groupAccepted u n = do - u <# ("'SimpleX Directory'> Joining the group " <> n <> "…") - u <## ("#" <> viewName n <> ": 'SimpleX Directory' joined the group") + u <### + [ WithTime ("'SimpleX Directory'> Joining the group " <> n <> "…"), + ConsoleString ("#" <> viewName n <> ": 'SimpleX Directory' joined the group") + ] u <# ("'SimpleX Directory'> Joined the group " <> n <> ", creating the link…") u <# "'SimpleX Directory'> Created the public link to join the group via this directory service that is always online." u <## "" diff --git a/tests/ChatClient.hs b/tests/ChatClient.hs index e258f3dccc..8408fc0098 100644 --- a/tests/ChatClient.hs +++ b/tests/ChatClient.hs @@ -84,7 +84,7 @@ schemaDumpDBOpts = DBOpts { connstr = B.pack testDBConnstr, schema = "test_chat_schema", - poolSize = 3, + poolSize = 10, createSchema = True } @@ -131,7 +131,7 @@ testCoreOpts = -- dbSchemaPrefix is not used in tests (except bot tests where it's redefined), -- instead different schema prefix is passed per client so that single test database is used dbSchemaPrefix = "", - dbPoolSize = 1, + dbPoolSize = 10, dbCreateSchema = True #else { dbFilePrefix = "./simplex_v1", -- dbFilePrefix is not used in tests (except bot tests where it's redefined) @@ -424,7 +424,10 @@ testChatN cfg opts ps test params = ( TestCC -> IO String -getTermLine cc@TestCC {printOutput} = +getTermLine = getTermLine' Nothing + +getTermLine' :: HasCallStack => Maybe String -> TestCC -> IO String +getTermLine' expected cc@TestCC {printOutput} = 5000000 `timeout` atomically (readTQueue $ termQ cc) >>= \case Just s -> do -- remove condition to always echo virtual terminal @@ -433,7 +436,12 @@ getTermLine cc@TestCC {printOutput} = name <- userName cc putStrLn $ name <> ": " <> s pure s - _ -> error "no output for 5 seconds" + Nothing -> do + name <- userName cc + let expectedMsg = case expected of + Just e -> ", expected: " <> show e + Nothing -> "" + error $ name <> ": no output for 5 seconds" <> expectedMsg userName :: TestCC -> IO [Char] userName (TestCC ChatController {currentUser} _ _ _ _ _) = diff --git a/tests/ChatTests/Groups.hs b/tests/ChatTests/Groups.hs index 121d5a92f8..16cf968437 100644 --- a/tests/ChatTests/Groups.hs +++ b/tests/ChatTests/Groups.hs @@ -2071,11 +2071,8 @@ testSharedMessageBody ps' = ] bob <# "#team alice> hello" cath <# "#team alice> hello" --- because of PostgreSQL concurrency deleteSndMsgDelivery fails to delete message body -#if !defined(dbPostgres) threadDelay 500000 checkMsgBodyCount alice 0 -#endif alice <## "disconnected 4 connections on server localhost" where @@ -2130,10 +2127,7 @@ testSharedBatchBody ps = concurrently_ (bob <# ("#team alice> message " <> show i)) (cath <# ("#team alice> message " <> show i)) --- because of PostgreSQL concurrency deleteSndMsgDelivery fails to delete message body -#if !defined(dbPostgres) checkMsgBodyCount alice 0 -#endif alice <## "disconnected 4 connections on server localhost" where diff --git a/tests/ChatTests/Utils.hs b/tests/ChatTests/Utils.hs index dded39e692..756ee47727 100644 --- a/tests/ChatTests/Utils.hs +++ b/tests/ChatTests/Utils.hs @@ -171,7 +171,8 @@ cc ?#> cmd = do (#$>) :: (Eq a, Show a, HasCallStack) => TestCC -> (String, String -> a, a) -> Expectation cc #$> (cmd, f, res) = do cc ##> cmd - (f <$> getTermLine cc) `shouldReturn` res + let expected = "result of " <> cmd <> ": " <> show res + (f <$> getTermLine' (Just expected) cc) `shouldReturn` res -- / PQ combinators @@ -345,7 +346,7 @@ chats = mapChats . read getChats :: HasCallStack => (Eq a, Show a) => ([(String, String, Maybe ConnStatus)] -> [a]) -> TestCC -> [a] -> Expectation getChats f cc res = do cc ##> "/_get chats 1 pcc=on" - line <- getTermLine cc + line <- getTermLine' (Just "chat list") cc f (read line) `shouldMatchList` res send :: TestCC -> String -> IO () @@ -353,41 +354,41 @@ send TestCC {chatController = cc} cmd = atomically $ writeTBQueue (inputQ cc) cm (<##) :: HasCallStack => TestCC -> String -> Expectation cc <## line = do - l <- getTermLine cc + l <- getTermLine' (Just line) cc when (l /= line) $ print ("expected: " <> line, ", got: " <> l) l `shouldBe` line (<##.) :: HasCallStack => TestCC -> String -> Expectation cc <##. line = do - l <- getTermLine cc + l <- getTermLine' (Just $ "prefix: " <> line) cc let prefix = line `isPrefixOf` l unless prefix $ print ("expected to start from: " <> line, ", got: " <> l) prefix `shouldBe` True (.<##) :: HasCallStack => TestCC -> String -> Expectation cc .<## line = do - l <- getTermLine cc + l <- getTermLine' (Just $ "suffix: " <> line) cc let suffix = line `isSuffixOf` l unless suffix $ print ("expected to end with: " <> line, ", got: " <> l) suffix `shouldBe` True (<#.) :: HasCallStack => TestCC -> String -> Expectation cc <#. line = do - l <- dropTime <$> getTermLine cc + l <- dropTime <$> getTermLine' (Just $ "prefix: " <> line) cc let prefix = line `isPrefixOf` l unless prefix $ print ("expected to start from: " <> line, ", got: " <> l) prefix `shouldBe` True (.<#) :: HasCallStack => TestCC -> String -> Expectation cc .<# line = do - l <- dropTime <$> getTermLine cc + l <- dropTime <$> getTermLine' (Just $ "suffix: " <> line) cc let suffix = line `isSuffixOf` l unless suffix $ print ("expected to end with: " <> line, ", got: " <> l) suffix `shouldBe` True (<##..) :: HasCallStack => TestCC -> [String] -> Expectation cc <##.. ls = do - l <- getTermLine cc + l <- getTermLine' (Just $ "one of prefixes: " <> show ls) cc let prefix = any (`isPrefixOf` l) ls unless prefix $ print ("expected to start from one of: " <> show ls, ", got: " <> l) prefix `shouldBe` True @@ -395,7 +396,8 @@ cc <##.. ls = do (>*) :: HasCallStack => TestCC -> String -> IO () cc >* note = do cc `send` ("/* " <> note) - (dropTime <$> getTermLine cc) `shouldReturn` ("* " <> note) + let expected = "* " <> note + (dropTime <$> getTermLine' (Just expected) cc) `shouldReturn` expected data ConsoleResponse = ConsoleString String @@ -404,13 +406,21 @@ data ConsoleResponse | StartsWith String | Predicate (String -> Bool) +instance Show ConsoleResponse where + show (ConsoleString s) = show s + show (WithTime s) = "WithTime " <> show s + show (EndsWith s) = "EndsWith " <> show s + show (StartsWith s) = "StartsWith " <> show s + show (Predicate _) = "" + instance IsString ConsoleResponse where fromString = ConsoleString -- this assumes that the string can only match one option getInAnyOrder :: HasCallStack => (String -> String) -> TestCC -> [ConsoleResponse] -> Expectation getInAnyOrder _ _ [] = pure () getInAnyOrder f cc ls = do - line <- f <$> getTermLine cc + let expectedDesc = "one of " <> show (length ls) <> " responses: " <> show ls + line <- f <$> getTermLine' (Just expectedDesc) cc let rest = filterFirst (expected line) ls if length rest < length ls then getInAnyOrder f cc rest @@ -436,25 +446,27 @@ getInAnyOrder f cc ls = do (<##?) = getInAnyOrder dropTime (<#) :: HasCallStack => TestCC -> String -> Expectation -cc <# line = (dropTime <$> getTermLine cc) `shouldReturn` line +cc <# line = (dropTime <$> getTermLine' (Just line) cc) `shouldReturn` line (*<#) :: HasCallStack => [TestCC] -> String -> Expectation ccs *<# line = mapConcurrently_ (<# line) ccs (?<#) :: HasCallStack => TestCC -> String -> Expectation -cc ?<# line = (dropTime <$> getTermLine cc) `shouldReturn` "i " <> line +cc ?<# line = do + let expected = "i " <> line + (dropTime <$> getTermLine' (Just expected) cc) `shouldReturn` expected ($<#) :: HasCallStack => (TestCC, String) -> String -> Expectation -(cc, uName) $<# line = (dropTime . dropUser uName <$> getTermLine cc) `shouldReturn` line +(cc, uName) $<# line = (dropTime . dropUser uName <$> getTermLine' (Just $ "for user " <> uName <> ": " <> line) cc) `shouldReturn` line (^<#) :: HasCallStack => (TestCC, String) -> String -> Expectation -(cc, p) ^<# line = (dropTime . dropStrPrefix p <$> getTermLine cc) `shouldReturn` line +(cc, p) ^<# line = (dropTime . dropStrPrefix p <$> getTermLine' (Just $ "without prefix " <> p <> ": " <> line) cc) `shouldReturn` line (⩗) :: HasCallStack => TestCC -> String -> Expectation -cc ⩗ line = (dropTime . dropReceipt <$> getTermLine cc) `shouldReturn` line +cc ⩗ line = (dropTime . dropReceipt <$> getTermLine' (Just $ "receipt: " <> line) cc) `shouldReturn` line (%) :: HasCallStack => TestCC -> String -> Expectation -cc % line = (dropTime . dropPartialReceipt <$> getTermLine cc) `shouldReturn` line +cc % line = (dropTime . dropPartialReceipt <$> getTermLine' (Just $ "partial receipt: " <> line) cc) `shouldReturn` line ( TestCC -> Expectation ( TestCC -> IO (String, String) getInvitations cc = do shortInv <- getInvitation_ cc cc <##. "The invitation link for old clients:" - fullInv <- getTermLine cc + fullInv <- getTermLine' (Just "full invitation link") cc pure (shortInv, fullInv) getInvitationNoShortLink :: HasCallStack => TestCC -> IO String @@ -537,7 +549,7 @@ getInvitation_ :: HasCallStack => TestCC -> IO String getInvitation_ cc = do cc <## "pass this invitation link to your contact (via another channel):" cc <## "" - inv <- getTermLine cc + inv <- getTermLine' (Just "invitation link") cc cc <## "" cc <## "and ask them to connect: /c " pure inv @@ -550,7 +562,8 @@ getContactLink cc created = do getContactLinks :: HasCallStack => TestCC -> Bool -> IO (String, String) getContactLinks cc created = do shortLink <- getContactLink_ cc created - fullLink <- dropLinePrefix "The contact link for old clients: " =<< getTermLine cc + line <- getTermLine' (Just "full contact link line") cc + fullLink <- dropLinePrefix "The contact link for old clients: " line pure (shortLink, fullLink) getContactLinkNoShortLink :: HasCallStack => TestCC -> Bool -> IO String @@ -560,7 +573,7 @@ getContactLink_ :: HasCallStack => TestCC -> Bool -> IO String getContactLink_ cc created = do cc <## if created then "Your new chat address is created!" else "Your chat address:" cc <## "" - link <- getTermLine cc + link <- getTermLine' (Just "contact link") cc cc <## "" cc <## "Anybody can send you contact requests with: /c " cc <## "to show it again: /sa" @@ -581,7 +594,8 @@ getGroupLink cc gName mRole created = do getGroupLinks :: HasCallStack => TestCC -> String -> GroupMemberRole -> Bool -> IO (String, String) getGroupLinks cc gName mRole created = do shortLink <- getGroupLink_ cc gName mRole created - fullLink <- dropLinePrefix "The group link for old clients: " =<< getTermLine cc + line <- getTermLine' (Just "full group link line") cc + fullLink <- dropLinePrefix "The group link for old clients: " line pure (shortLink, fullLink) getGroupLinkNoShortLink :: HasCallStack => TestCC -> String -> GroupMemberRole -> Bool -> IO String @@ -591,7 +605,7 @@ getGroupLink_ :: HasCallStack => TestCC -> String -> GroupMemberRole -> Bool -> getGroupLink_ cc gName mRole created = do cc <## if created then "Group link is created!" else "Group link:" cc <## "" - link <- getTermLine cc + link <- getTermLine' (Just $ "group link for " <> gName) cc cc <## "" cc <## ("Anybody can connect to you and join group as " <> T.unpack (textEncode mRole) <> " with: /c ") cc <## ("to show it again: /show link #" <> gName) @@ -669,7 +683,7 @@ getTestCCContact cc contactId = do lastItemId :: HasCallStack => TestCC -> IO String lastItemId cc = do cc ##> "/last_item_id" - getTermLine cc + getTermLine' (Just "last item id") cc showActiveUser :: HasCallStack => TestCC -> String -> Expectation showActiveUser cc name = do From 405ce9615e0bb0c6e27a266d696f279f97ad1ad0 Mon Sep 17 00:00:00 2001 From: Evgeny Date: Fri, 9 Jan 2026 08:24:26 +0000 Subject: [PATCH 07/73] core: support content filter for contacts and notes to allow media galleries (#6552) * core: support content filter for contacts and notes to allow media galleries * add api to list chat content types, tests * query plans, api docs * add indices --- bots/src/API/Docs/Commands.hs | 1 + bots/src/API/Docs/Responses.hs | 1 + simplex-chat.cabal | 2 + src/Simplex/Chat/Controller.hs | 2 + src/Simplex/Chat/Library/Commands.hs | 9 +- src/Simplex/Chat/Protocol.hs | 2 + src/Simplex/Chat/Store/Groups.hs | 1 - src/Simplex/Chat/Store/Messages.hs | 393 ++++++++---------- src/Simplex/Chat/Store/Postgres/Migrations.hs | 4 +- .../Migrations/M20260108_chat_indices.hs | 33 ++ .../Store/Postgres/Migrations/chat_schema.sql | 8 + src/Simplex/Chat/Store/SQLite/Migrations.hs | 4 +- .../Migrations/M20260108_chat_indices.hs | 32 ++ .../SQLite/Migrations/agent_query_plans.txt | 128 +++--- .../SQLite/Migrations/chat_query_plans.txt | 192 ++++----- .../Store/SQLite/Migrations/chat_schema.sql | 12 + src/Simplex/Chat/View.hs | 1 + tests/ChatTests/Files.hs | 40 +- tests/ChatTests/Local.hs | 11 +- 19 files changed, 488 insertions(+), 388 deletions(-) create mode 100644 src/Simplex/Chat/Store/Postgres/Migrations/M20260108_chat_indices.hs create mode 100644 src/Simplex/Chat/Store/SQLite/Migrations/M20260108_chat_indices.hs diff --git a/bots/src/API/Docs/Commands.hs b/bots/src/API/Docs/Commands.hs index 4cce44e588..41e8cdf019 100644 --- a/bots/src/API/Docs/Commands.hs +++ b/bots/src/API/Docs/Commands.hs @@ -340,6 +340,7 @@ undocumentedCommands = "APIGetAppSettings", "APIGetCallInvitations", "APIGetChat", + "APIGetChatContentTypes", "APIGetChatItemInfo", "APIGetChatItems", "APIGetChatItemTTL", diff --git a/bots/src/API/Docs/Responses.hs b/bots/src/API/Docs/Responses.hs index 154d44b6c2..c52b288603 100644 --- a/bots/src/API/Docs/Responses.hs +++ b/bots/src/API/Docs/Responses.hs @@ -121,6 +121,7 @@ undocumentedResponses = "CRBroadcastSent", "CRCallInvitations", "CRChatCleared", + "CRChatContentTypes", "CRChatHelp", "CRChatItemId", "CRChatItemInfo", diff --git a/simplex-chat.cabal b/simplex-chat.cabal index 95f386e9df..c021023e49 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -125,6 +125,7 @@ library Simplex.Chat.Store.Postgres.Migrations.M20251117_member_relations_vector Simplex.Chat.Store.Postgres.Migrations.M20251128_migrate_member_relations Simplex.Chat.Store.Postgres.Migrations.M20251230_strict_tables + Simplex.Chat.Store.Postgres.Migrations.M20260108_chat_indices else exposed-modules: Simplex.Chat.Archive @@ -273,6 +274,7 @@ library Simplex.Chat.Store.SQLite.Migrations.M20251117_member_relations_vector Simplex.Chat.Store.SQLite.Migrations.M20251128_migrate_member_relations Simplex.Chat.Store.SQLite.Migrations.M20251230_strict_tables + Simplex.Chat.Store.SQLite.Migrations.M20260108_chat_indices other-modules: Paths_simplex_chat hs-source-dirs: diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index 30df251fbb..9703226e1a 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -316,6 +316,7 @@ data ChatCommand | APIGetChatTags UserId | APIGetChats {userId :: UserId, pendingConnections :: Bool, pagination :: PaginationByTime, query :: ChatListQuery} | APIGetChat {chatRef :: ChatRef, contentTag :: Maybe MsgContentTag, chatPagination :: ChatPagination, search :: Maybe Text} + | APIGetChatContentTypes ChatRef | APIGetChatItems {chatPagination :: ChatPagination, search :: Maybe Text} | APIGetChatItemInfo {chatRef :: ChatRef, chatItemId :: ChatItemId} | APISendMessages {sendRef :: SendRef, liveMessage :: Bool, ttl :: Maybe Int, composedMessages :: NonEmpty ComposedMessage} @@ -637,6 +638,7 @@ data ChatResponse | CRApiChats {user :: User, chats :: [AChat]} | CRChats {chats :: [AChat]} | CRApiChat {user :: User, chat :: AChat, navInfo :: Maybe NavigationInfo} + | CRChatContentTypes {contentTypes :: [MsgContentTag]} | CRChatTags {user :: User, userTags :: [ChatTag]} | CRChatItems {user :: User, chatName_ :: Maybe ChatName, chatItems :: [AChatItem]} | CRChatItemInfo {user :: User, chatItem :: AChatItem, chatItemInfo :: ChatItemInfo} diff --git a/src/Simplex/Chat/Library/Commands.hs b/src/Simplex/Chat/Library/Commands.hs index 675ca03a8a..994b435297 100644 --- a/src/Simplex/Chat/Library/Commands.hs +++ b/src/Simplex/Chat/Library/Commands.hs @@ -535,16 +535,14 @@ processChatCommand vr nm = \case APIGetChat (ChatRef cType cId scope_) contentFilter pagination search -> withUser $ \user -> case cType of -- TODO optimize queries calculating ChatStats, currently they're disabled CTDirect -> do - when (isJust contentFilter) $ throwCmdError "content filter not supported" - (directChat, navInfo) <- withFastStore (\db -> getDirectChat db vr user cId pagination search) + (directChat, navInfo) <- withFastStore (\db -> getDirectChat db vr user cId contentFilter pagination search) pure $ CRApiChat user (AChat SCTDirect directChat) navInfo CTGroup -> do (groupChat, navInfo) <- withFastStore (\db -> getGroupChat db vr user cId scope_ contentFilter pagination search) groupChat' <- checkSupportChatAttention user groupChat pure $ CRApiChat user (AChat SCTGroup groupChat') navInfo CTLocal -> do - when (isJust contentFilter) $ throwCmdError "content filter not supported" - (localChat, navInfo) <- withFastStore (\db -> getLocalChat db user cId pagination search) + (localChat, navInfo) <- withFastStore (\db -> getLocalChat db user cId contentFilter pagination search) pure $ CRApiChat user (AChat SCTLocal localChat) navInfo CTContactRequest -> throwCmdError "not implemented" CTContactConnection -> throwCmdError "not supported" @@ -570,6 +568,8 @@ processChatCommand vr nm = \case newFromMember (CChatItem _ ChatItem {chatDir = CIGroupRcv m, meta = CIMeta {itemStatus = CISRcvNew}}) = groupMemberId' m == scopeGMId newFromMember _ = False + APIGetChatContentTypes chatRef -> withUser $ \user -> + CRChatContentTypes <$> withStore (\db -> getChatContentTypes db user chatRef) APIGetChatItems pagination search -> withUser $ \user -> do chatItems <- withFastStore $ \db -> getAllChatItems db vr user pagination search pure $ CRChatItems user Nothing chatItems @@ -4368,6 +4368,7 @@ chatCommandP = <*> (A.space *> jsonP <|> pure clqNoFilters) ), "/_get chat " *> (APIGetChat <$> chatRefP <*> optional (" content=" *> strP) <* A.space <*> chatPaginationP <*> optional (" search=" *> textP)), + "/_get content types " *> (APIGetChatContentTypes <$> chatRefP), "/_get items " *> (APIGetChatItems <$> chatPaginationP <*> optional (" search=" *> textP)), "/_get item info " *> (APIGetChatItemInfo <$> chatRefP <* A.space <*> A.decimal), "/_send " *> (APISendMessages <$> sendRefP <*> liveMessageP <*> sendMessageTTLP <*> (" json " *> jsonP <|> " text " *> composedMessagesTextP)), diff --git a/src/Simplex/Chat/Protocol.hs b/src/Simplex/Chat/Protocol.hs index 2aafad94a1..40b667365e 100644 --- a/src/Simplex/Chat/Protocol.hs +++ b/src/Simplex/Chat/Protocol.hs @@ -518,6 +518,8 @@ instance ToJSON MsgContentTag where toJSON = strToJSON toEncoding = strToJEncoding +instance FromField MsgContentTag where fromField = fromTextField_ $ eitherToMaybe . strDecode . encodeUtf8 + instance ToField MsgContentTag where toField = toField . safeDecodeUtf8 . strEncode data MsgContainer diff --git a/src/Simplex/Chat/Store/Groups.hs b/src/Simplex/Chat/Store/Groups.hs index fadc65960b..5721efb65e 100644 --- a/src/Simplex/Chat/Store/Groups.hs +++ b/src/Simplex/Chat/Store/Groups.hs @@ -184,7 +184,6 @@ import Simplex.Messaging.Util (eitherToMaybe, firstRow', safeDecodeUtf8, ($>>=), import Simplex.Messaging.Version import UnliftIO.STM #if defined(dbPostgres) -import qualified Data.Set as S import Database.PostgreSQL.Simple (In (..), Only (..), Query, (:.) (..)) import Database.PostgreSQL.Simple.SqlQQ (sql) #else diff --git a/src/Simplex/Chat/Store/Messages.hs b/src/Simplex/Chat/Store/Messages.hs index e3b6911b59..71e8a35386 100644 --- a/src/Simplex/Chat/Store/Messages.hs +++ b/src/Simplex/Chat/Store/Messages.hs @@ -45,6 +45,7 @@ module Simplex.Chat.Store.Messages createNewChatItem_, getChatPreviews, checkContactHasItems, + getChatContentTypes, getDirectChat, getGroupChat, getGroupChatScopeInfoForItem, @@ -1166,40 +1167,44 @@ checkContactHasItems db User {userId} Contact {contactId} = "SELECT EXISTS (SELECT 1 FROM chat_items WHERE user_id = ? AND contact_id = ?)" (userId, contactId) -getDirectChat :: DB.Connection -> VersionRangeChat -> User -> Int64 -> ChatPagination -> Maybe Text -> ExceptT StoreError IO (Chat 'CTDirect, Maybe NavigationInfo) -getDirectChat db vr user contactId pagination search_ = do +getChatContentTypes :: DB.Connection -> User -> ChatRef -> ExceptT StoreError IO [MsgContentTag] +getChatContentTypes db User {userId} (ChatRef cType chatId chatScope_) = case cType of + CTDirect -> getTypes " contact_id = ? " () + CTLocal -> getTypes " note_folder_id = ? " () + CTGroup -> case chatScope_ of + Nothing -> getTypes " group_id = ? AND group_scope_tag IS NULL AND group_scope_group_member_id IS NULL " () + Just (GCSMemberSupport mId_) -> getTypes " group_id = ? AND group_scope_tag = ? AND group_scope_group_member_id IS NOT DISTINCT FROM ? " (GCSTMemberSupport_, mId_) + _ -> throwError $ SEInternalError "unsupported chat type" + where + getTypes :: ToRow p => Query -> p -> ExceptT StoreError IO [MsgContentTag] + getTypes cond params = + liftIO $ map fromOnly + <$> DB.query + db + ("SELECT DISTINCT msg_content_tag FROM chat_items WHERE user_id = ? AND " <> cond <> " AND msg_content_tag IS NOT NULL ORDER BY msg_content_tag") + ((userId, chatId) :. params) + +getDirectChat :: DB.Connection -> VersionRangeChat -> User -> Int64 -> Maybe MsgContentTag -> ChatPagination -> Maybe Text -> ExceptT StoreError IO (Chat 'CTDirect, Maybe NavigationInfo) +getDirectChat db vr user contactId contentFilter pagination search_ = do let search = fromMaybe "" search_ ct <- getContact db vr user contactId case pagination of - CPLast count -> liftIO $ (,Nothing) <$> getDirectChatLast_ db user ct count search - CPAfter afterId count -> (,Nothing) <$> getDirectChatAfter_ db user ct afterId count search - CPBefore beforeId count -> (,Nothing) <$> getDirectChatBefore_ db user ct beforeId count search - CPAround aroundId count -> getDirectChatAround_ db user ct aroundId count search + CPLast count -> (,Nothing) <$> getDirectChatLast_ db user ct contentFilter count search + CPAfter afterId count -> (,Nothing) <$> getDirectChatAfter_ db user ct contentFilter afterId count search + CPBefore beforeId count -> (,Nothing) <$> getDirectChatBefore_ db user ct contentFilter beforeId count search + CPAround aroundId count -> getDirectChatAround_ db user ct contentFilter aroundId count search CPInitial count -> do unless (T.null search) $ throwError $ SEInternalError "initial chat pagination doesn't support search" - getDirectChatInitial_ db user ct count + getDirectChatInitial_ db user ct contentFilter count -- the last items in reverse order (the last item in the conversation is the first in the returned list) -getDirectChatLast_ :: DB.Connection -> User -> Contact -> Int -> Text -> IO (Chat 'CTDirect) -getDirectChatLast_ db user ct count search = do - ciIds <- getDirectChatItemIdsLast_ db user ct count search - ts <- getCurrentTime - cis <- mapM (safeGetDirectItem db user ct ts) ciIds - pure $ Chat (DirectChat ct) (reverse cis) emptyChatStats - -getDirectChatItemIdsLast_ :: DB.Connection -> User -> Contact -> Int -> Text -> IO [ChatItemId] -getDirectChatItemIdsLast_ db User {userId} Contact {contactId} count search = - map fromOnly - <$> DB.query - db - [sql| - SELECT chat_item_id - FROM chat_items - WHERE user_id = ? AND contact_id = ? AND LOWER(item_text) LIKE '%' || LOWER(?) || '%' - ORDER BY created_at DESC, chat_item_id DESC - LIMIT ? - |] - (userId, contactId, search, count) +getDirectChatLast_ :: DB.Connection -> User -> Contact -> Maybe MsgContentTag -> Int -> Text -> ExceptT StoreError IO (Chat 'CTDirect) +getDirectChatLast_ db user ct contentFilter count search = do + let cInfo = DirectChat ct + ciIds <- getChatItemIDs db user cInfo contentFilter CRLast count search + ts <- liftIO getCurrentTime + cis <- liftIO $ mapM (safeGetDirectItem db user ct ts) ciIds + pure $ Chat cInfo (reverse cis) emptyChatStats safeGetDirectItem :: DB.Connection -> User -> Contact -> UTCTime -> ChatItemId -> IO (CChatItem 'CTDirect) safeGetDirectItem db user ct currentTs itemId = @@ -1248,81 +1253,57 @@ getDirectChatItemLast db user@User {userId} contactId = do (userId, contactId) getDirectChatItem db user contactId chatItemId -getDirectChatAfter_ :: DB.Connection -> User -> Contact -> ChatItemId -> Int -> Text -> ExceptT StoreError IO (Chat 'CTDirect) -getDirectChatAfter_ db user ct@Contact {contactId} afterId count search = do +getDirectChatAfter_ :: DB.Connection -> User -> Contact -> Maybe MsgContentTag -> ChatItemId -> Int -> Text -> ExceptT StoreError IO (Chat 'CTDirect) +getDirectChatAfter_ db user ct@Contact {contactId} contentFilter afterId count search = do afterCI <- getDirectChatItem db user contactId afterId - ciIds <- liftIO $ getDirectCIsAfter_ db user ct afterCI count search + let cInfo = DirectChat ct + range = CRAfter (ciCreatedAt afterCI) (cChatItemId afterCI) + ciIds <- getChatItemIDs db user cInfo contentFilter range count search ts <- liftIO getCurrentTime cis <- liftIO $ mapM (safeGetDirectItem db user ct ts) ciIds - pure $ Chat (DirectChat ct) cis emptyChatStats + pure $ Chat cInfo cis emptyChatStats -getDirectCIsAfter_ :: DB.Connection -> User -> Contact -> CChatItem 'CTDirect -> Int -> Text -> IO [ChatItemId] -getDirectCIsAfter_ db User {userId} Contact {contactId} afterCI count search = - map fromOnly - <$> DB.query - db - [sql| - SELECT chat_item_id - FROM chat_items - WHERE user_id = ? AND contact_id = ? AND LOWER(item_text) LIKE '%' || LOWER(?) || '%' - AND (created_at > ? OR (created_at = ? AND chat_item_id > ?)) - ORDER BY created_at ASC, chat_item_id ASC - LIMIT ? - |] - (userId, contactId, search, ciCreatedAt afterCI, ciCreatedAt afterCI, cChatItemId afterCI, count) - -getDirectChatBefore_ :: DB.Connection -> User -> Contact -> ChatItemId -> Int -> Text -> ExceptT StoreError IO (Chat 'CTDirect) -getDirectChatBefore_ db user ct@Contact {contactId} beforeId count search = do +getDirectChatBefore_ :: DB.Connection -> User -> Contact -> Maybe MsgContentTag -> ChatItemId -> Int -> Text -> ExceptT StoreError IO (Chat 'CTDirect) +getDirectChatBefore_ db user ct@Contact {contactId} contentFilter beforeId count search = do beforeCI <- getDirectChatItem db user contactId beforeId - ciIds <- liftIO $ getDirectCIsBefore_ db user ct beforeCI count search + let cInfo = DirectChat ct + range = CRBefore (ciCreatedAt beforeCI) (cChatItemId beforeCI) + ciIds <- getChatItemIDs db user cInfo contentFilter range count search ts <- liftIO getCurrentTime cis <- liftIO $ mapM (safeGetDirectItem db user ct ts) ciIds - pure $ Chat (DirectChat ct) (reverse cis) emptyChatStats + pure $ Chat cInfo (reverse cis) emptyChatStats -getDirectCIsBefore_ :: DB.Connection -> User -> Contact -> CChatItem 'CTDirect -> Int -> Text -> IO [ChatItemId] -getDirectCIsBefore_ db User {userId} Contact {contactId} beforeCI count search = - map fromOnly - <$> DB.query - db - [sql| - SELECT chat_item_id - FROM chat_items - WHERE user_id = ? AND contact_id = ? AND LOWER(item_text) LIKE '%' || LOWER(?) || '%' - AND (created_at < ? OR (created_at = ? AND chat_item_id < ?)) - ORDER BY created_at DESC, chat_item_id DESC - LIMIT ? - |] - (userId, contactId, search, ciCreatedAt beforeCI, ciCreatedAt beforeCI, cChatItemId beforeCI, count) - -getDirectChatAround_ :: DB.Connection -> User -> Contact -> ChatItemId -> Int -> Text -> ExceptT StoreError IO (Chat 'CTDirect, Maybe NavigationInfo) -getDirectChatAround_ db user ct aroundId count search = do +getDirectChatAround_ :: DB.Connection -> User -> Contact -> Maybe MsgContentTag -> ChatItemId -> Int -> Text -> ExceptT StoreError IO (Chat 'CTDirect, Maybe NavigationInfo) +getDirectChatAround_ db user ct contentFilter aroundId count search = do stats <- liftIO $ getContactStats_ db user ct - getDirectChatAround' db user ct aroundId count search stats + getDirectChatAround' db user ct contentFilter aroundId count search stats -getDirectChatAround' :: DB.Connection -> User -> Contact -> ChatItemId -> Int -> Text -> ChatStats -> ExceptT StoreError IO (Chat 'CTDirect, Maybe NavigationInfo) -getDirectChatAround' db user ct@Contact {contactId} aroundId count search stats = do +getDirectChatAround' :: DB.Connection -> User -> Contact -> Maybe MsgContentTag -> ChatItemId -> Int -> Text -> ChatStats -> ExceptT StoreError IO (Chat 'CTDirect, Maybe NavigationInfo) +getDirectChatAround' db user ct@Contact {contactId} contentFilter aroundId count search stats = do aroundCI <- getDirectChatItem db user contactId aroundId - beforeIds <- liftIO $ getDirectCIsBefore_ db user ct aroundCI count search - afterIds <- liftIO $ getDirectCIsAfter_ db user ct aroundCI count search + let cInfo = DirectChat ct + range r = r (ciCreatedAt aroundCI) (cChatItemId aroundCI) + beforeIds <- getChatItemIDs db user cInfo contentFilter (range CRBefore) count search + afterIds <- getChatItemIDs db user cInfo contentFilter (range CRAfter) count search ts <- liftIO getCurrentTime beforeCIs <- liftIO $ mapM (safeGetDirectItem db user ct ts) beforeIds afterCIs <- liftIO $ mapM (safeGetDirectItem db user ct ts) afterIds let cis = reverse beforeCIs <> [aroundCI] <> afterCIs navInfo <- liftIO $ getNavInfo cis - pure (Chat (DirectChat ct) cis stats, Just navInfo) + pure (Chat cInfo cis stats, Just navInfo) where getNavInfo cis_ = case cis_ of [] -> pure $ NavigationInfo 0 0 cis -> getContactNavInfo_ db user ct (last cis) -getDirectChatInitial_ :: DB.Connection -> User -> Contact -> Int -> ExceptT StoreError IO (Chat 'CTDirect, Maybe NavigationInfo) -getDirectChatInitial_ db user ct count = do +getDirectChatInitial_ :: DB.Connection -> User -> Contact -> Maybe MsgContentTag -> Int -> ExceptT StoreError IO (Chat 'CTDirect, Maybe NavigationInfo) +getDirectChatInitial_ db user ct contentFilter count = do liftIO (getContactMinUnreadId_ db user ct) >>= \case Just minUnreadItemId -> do unreadCount <- liftIO $ getContactUnreadCount_ db user ct let stats = emptyChatStats {unreadCount, minUnreadItemId} - getDirectChatAround' db user ct minUnreadItemId count "" stats - Nothing -> liftIO $ (,Just $ NavigationInfo 0 0) <$> getDirectChatLast_ db user ct count "" + getDirectChatAround' db user ct contentFilter minUnreadItemId count "" stats + Nothing -> (,Just $ NavigationInfo 0 0) <$> getDirectChatLast_ db user ct contentFilter count "" getContactStats_ :: DB.Connection -> User -> Contact -> IO ChatStats getContactStats_ db user ct = do @@ -1471,64 +1452,81 @@ getGroupChatScopeForItem_ db itemId = getGroupChatLast_ :: DB.Connection -> User -> GroupInfo -> Maybe GroupChatScopeInfo -> Maybe MsgContentTag -> Int -> Text -> ChatStats -> ExceptT StoreError IO (Chat 'CTGroup) getGroupChatLast_ db user g scopeInfo_ contentFilter count search stats = do - ciIds <- getGroupChatItemIDs db user g scopeInfo_ contentFilter GRLast count search + let cInfo = GroupChat g scopeInfo_ + ciIds <- getChatItemIDs db user cInfo contentFilter CRLast count search ts <- liftIO getCurrentTime cis <- mapM (liftIO . safeGetGroupItem db user g ts) ciIds - pure $ Chat (GroupChat g scopeInfo_) (reverse cis) stats + pure $ Chat cInfo (reverse cis) stats -data GroupItemIDsRange = GRLast | GRAfter UTCTime ChatItemId | GRBefore UTCTime ChatItemId +data ChatItemIDsRange = CRLast | CRAfter UTCTime ChatItemId | CRBefore UTCTime ChatItemId -getGroupChatItemIDs :: DB.Connection -> User -> GroupInfo -> Maybe GroupChatScopeInfo -> Maybe MsgContentTag -> GroupItemIDsRange -> Int -> Text -> ExceptT StoreError IO [ChatItemId] -getGroupChatItemIDs db User {userId} GroupInfo {groupId} scopeInfo_ contentFilter range count search = case (scopeInfo_, contentFilter) of - (Nothing, Nothing) -> - liftIO $ - idsQuery - (baseCond <> " AND group_scope_tag IS NULL AND group_scope_group_member_id IS NULL ") - (userId, groupId) - (Nothing, Just mcTag) -> - liftIO $ - idsQuery - (baseCond <> " AND msg_content_tag = ? ") - (userId, groupId, mcTag) - (Just GCSIMemberSupport {groupMember_ = Just m}, Nothing) -> - liftIO $ - idsQuery - (baseCond <> " AND group_scope_tag = ? AND group_scope_group_member_id = ? ") - (userId, groupId, GCSTMemberSupport_, groupMemberId' m) - (Just GCSIMemberSupport {groupMember_ = Nothing}, Nothing) -> - liftIO $ - idsQuery - (baseCond <> " AND group_scope_tag = ? AND group_scope_group_member_id IS NULL ") - (userId, groupId, GCSTMemberSupport_) - (Just _scope, Just _mcTag) -> - throwError $ SEInternalError "group scope and content filter are not supported together" +getChatItemIDs :: DB.Connection -> User -> ChatInfo c -> Maybe MsgContentTag -> ChatItemIDsRange -> Int -> Text -> ExceptT StoreError IO [ChatItemId] +getChatItemIDs db User {userId} cInfo contentFilter range count search = case cInfo of + GroupChat GroupInfo {groupId} scopeInfo_ -> case (scopeInfo_, contentFilter) of + (Nothing, Nothing) -> + liftIO $ + idsQuery + (grCond <> " AND group_scope_tag IS NULL AND group_scope_group_member_id IS NULL ") + (userId, groupId) + "item_ts" + (Nothing, Just mcTag) -> + liftIO $ + idsQuery + (grCond <> " AND msg_content_tag = ? ") + (userId, groupId, mcTag) + "item_ts" + (Just GCSIMemberSupport {groupMember_ = m}, Nothing) -> + liftIO $ + idsQuery + (grCond <> " AND group_scope_tag = ? AND group_scope_group_member_id IS NOT DISTINCT FROM ? ") + (userId, groupId, GCSTMemberSupport_, groupMemberId' <$> m) + "item_ts" + (Just _scope, Just _mcTag) -> + throwError $ SEInternalError "group scope and content filter are not supported together" + where + grCond = " user_id = ? AND group_id = ? " + DirectChat Contact {contactId} -> liftIO $ case contentFilter of + Nothing -> idsQuery ctCond (userId, contactId) "created_at" + Just mcTag -> idsQuery (ctCond <> " AND msg_content_tag = ? ") (userId, contactId, mcTag) "created_at" + where + ctCond = " user_id = ? AND contact_id = ? " + LocalChat NoteFolder {noteFolderId} -> liftIO $ case contentFilter of + Nothing -> idsQuery nfCond (userId, noteFolderId) "created_at" + Just mcTag -> idsQuery (nfCond <> " AND msg_content_tag = ? ") (userId, noteFolderId, mcTag) "created_at" + where + nfCond = " user_id = ? AND note_folder_id = ? " + _ -> throwError $ SEInternalError "unsupported chat type" where baseQuery = " SELECT chat_item_id FROM chat_items WHERE " - baseCond = " user_id = ? AND group_id = ? " - idsQuery :: ToRow p => Query -> p -> IO [ChatItemId] - idsQuery c p = case range of - GRLast -> rangeQuery c p " ORDER BY item_ts DESC, chat_item_id DESC " - GRAfter ts itemId -> + -- parameterized by timestamp field `f` used to order chat items: + -- `item_ts` for groups, `created_at` for direct chats and notes. + idsQuery :: ToRow p => Query -> p -> Query -> IO [ChatItemId] + idsQuery c p f = case range of + CRLast -> rangeQuery c p (" ORDER BY " <> f <> " DESC, chat_item_id DESC ") + CRAfter ts itemId -> rangeQuery - (" item_ts > ? " `orCond` " item_ts = ? AND chat_item_id > ? ") + ((f <> " > ?") `orCond` (f <> " = ? AND chat_item_id > ?")) (orParams ts itemId) - " ORDER BY item_ts ASC, chat_item_id ASC " - GRBefore ts itemId -> + (" ORDER BY " <> f <> " ASC, chat_item_id ASC ") + CRBefore ts itemId -> rangeQuery - (" item_ts < ? " `orCond` " item_ts = ? AND chat_item_id < ? ") + ((f <> " < ?") `orCond` (f <> " = ? AND chat_item_id < ?")) (orParams ts itemId) - " ORDER BY item_ts DESC, chat_item_id DESC " + (" ORDER BY " <> f <> " DESC, chat_item_id DESC ") where + -- `orCond` creates this query: `(c AND c1) OR (c AND c2)`, + -- that is equivalent to `c AND (c1 OR c2)`. + -- OR has to be used on the top level for query planner to use indices + -- that include fields in c1 and c2. orCond c1 c2 = " ((" <> c <> " AND " <> c1 <> ") OR (" <> c <> " AND " <> c2 <> ")) " orParams ts itemId = (p :. (Only ts) :. p :. (ts, itemId)) rangeQuery :: ToRow p => Query -> p -> Query -> IO [ChatItemId] - rangeQuery c p ob - | T.null search = searchQuery "" () - | otherwise = searchQuery " AND LOWER(item_text) LIKE '%' || LOWER(?) || '%' " (Only search) - where - searchQuery :: ToRow p' => Query -> p' -> IO [ChatItemId] - searchQuery c' p' = - map fromOnly <$> DB.query db (baseQuery <> c <> c' <> ob <> " LIMIT ?") (p :. p' :. Only count) + rangeQuery c p ob = + map fromOnly + <$> if T.null search + then DB.query db (baseQuery <> c <> ob <> " LIMIT ?") (p :. Only count) + else DB.query db (baseQuery <> c <> searchCond <> ob <> " LIMIT ?") (p :. (search, count)) + searchCond = " AND LOWER(item_text) LIKE '%' || LOWER(?) || '%' " safeGetGroupItem :: DB.Connection -> User -> GroupInfo -> UTCTime -> ChatItemId -> IO (CChatItem 'CTGroup) safeGetGroupItem db user g currentTs itemId = @@ -1580,20 +1578,22 @@ getGroupMemberChatItemLast db user@User {userId} groupId groupMemberId = do getGroupChatAfter_ :: DB.Connection -> User -> GroupInfo -> Maybe GroupChatScopeInfo -> Maybe MsgContentTag -> ChatItemId -> Int -> Text -> ExceptT StoreError IO (Chat 'CTGroup) getGroupChatAfter_ db user g@GroupInfo {groupId} scopeInfo contentFilter afterId count search = do afterCI <- getGroupChatItem db user groupId afterId - let range = GRAfter (chatItemTs afterCI) (cChatItemId afterCI) - ciIds <- getGroupChatItemIDs db user g scopeInfo contentFilter range count search + let cInfo = GroupChat g scopeInfo + range = CRAfter (chatItemTs afterCI) (cChatItemId afterCI) + ciIds <- getChatItemIDs db user cInfo contentFilter range count search ts <- liftIO getCurrentTime cis <- liftIO $ mapM (safeGetGroupItem db user g ts) ciIds - pure $ Chat (GroupChat g scopeInfo) cis emptyChatStats + pure $ Chat cInfo cis emptyChatStats getGroupChatBefore_ :: DB.Connection -> User -> GroupInfo -> Maybe GroupChatScopeInfo -> Maybe MsgContentTag -> ChatItemId -> Int -> Text -> ExceptT StoreError IO (Chat 'CTGroup) getGroupChatBefore_ db user g@GroupInfo {groupId} scopeInfo contentFilter beforeId count search = do beforeCI <- getGroupChatItem db user groupId beforeId - let range = GRBefore (chatItemTs beforeCI) (cChatItemId beforeCI) - ciIds <- getGroupChatItemIDs db user g scopeInfo contentFilter range count search + let cInfo = GroupChat g scopeInfo + range = CRBefore (chatItemTs beforeCI) (cChatItemId beforeCI) + ciIds <- getChatItemIDs db user cInfo contentFilter range count search ts <- liftIO getCurrentTime cis <- liftIO $ mapM (safeGetGroupItem db user g ts) ciIds - pure $ Chat (GroupChat g scopeInfo) (reverse cis) emptyChatStats + pure $ Chat cInfo (reverse cis) emptyChatStats getGroupChatAround_ :: DB.Connection -> User -> GroupInfo -> Maybe GroupChatScopeInfo -> Maybe MsgContentTag -> ChatItemId -> Int -> Text -> ExceptT StoreError IO (Chat 'CTGroup, Maybe NavigationInfo) getGroupChatAround_ db user g scopeInfo contentFilter aroundId count search = do @@ -1603,16 +1603,16 @@ getGroupChatAround_ db user g scopeInfo contentFilter aroundId count search = do getGroupChatAround' :: DB.Connection -> User -> GroupInfo -> Maybe GroupChatScopeInfo -> Maybe MsgContentTag -> ChatItemId -> Int -> Text -> ChatStats -> ExceptT StoreError IO (Chat 'CTGroup, Maybe NavigationInfo) getGroupChatAround' db user g scopeInfo contentFilter aroundId count search stats = do aroundCI <- getGroupCIWithReactions db user g aroundId - let beforeRange = GRBefore (chatItemTs aroundCI) (cChatItemId aroundCI) - afterRange = GRAfter (chatItemTs aroundCI) (cChatItemId aroundCI) - beforeIds <- getGroupChatItemIDs db user g scopeInfo contentFilter beforeRange count search - afterIds <- getGroupChatItemIDs db user g scopeInfo contentFilter afterRange count search + let cInfo = GroupChat g scopeInfo + range r = r (chatItemTs aroundCI) (cChatItemId aroundCI) + beforeIds <- getChatItemIDs db user cInfo contentFilter (range CRBefore) count search + afterIds <- getChatItemIDs db user cInfo contentFilter (range CRAfter) count search ts <- liftIO getCurrentTime beforeCIs <- liftIO $ mapM (safeGetGroupItem db user g ts) beforeIds afterCIs <- liftIO $ mapM (safeGetGroupItem db user g ts) afterIds let cis = reverse beforeCIs <> [aroundCI] <> afterCIs navInfo <- liftIO $ getNavInfo cis - pure (Chat (GroupChat g scopeInfo) cis stats, Just navInfo) + pure (Chat cInfo cis stats, Just navInfo) where getNavInfo cis_ = case cis_ of [] -> pure $ NavigationInfo 0 0 @@ -1677,18 +1677,12 @@ queryUnreadGroupItems db User {userId} GroupInfo {groupId} scopeInfo_ contentFil db (baseQuery <> " AND msg_content_tag = ? AND item_status = ? " <> orderLimit) (userId, groupId, mcTag, CISRcvNew) - (Just GCSIMemberSupport {groupMember_ = Just m}, Nothing) -> + (Just GCSIMemberSupport {groupMember_ = m}, Nothing) -> liftIO $ DB.query db - (baseQuery <> " AND group_scope_tag = ? AND group_scope_group_member_id = ? AND item_status = ? " <> orderLimit) - (userId, groupId, GCSTMemberSupport_, groupMemberId' m, CISRcvNew) - (Just GCSIMemberSupport {groupMember_ = Nothing}, Nothing) -> - liftIO $ - DB.query - db - (baseQuery <> " AND group_scope_tag = ? AND group_scope_group_member_id IS NULL AND item_status = ? " <> orderLimit) - (userId, groupId, GCSTMemberSupport_, CISRcvNew) + (baseQuery <> " AND group_scope_tag = ? AND group_scope_group_member_id IS NOT DISTINCT FROM ? AND item_status = ? " <> orderLimit) + (userId, groupId, GCSTMemberSupport_, groupMemberId' <$> m, CISRcvNew) (Just _scope, Just _mcTag) -> throwError $ SEInternalError "group scope and content filter are not supported together" @@ -1743,39 +1737,26 @@ getGroupNavInfo_ db User {userId} GroupInfo {groupId} afterCI = do :. (userId, groupId, chatItemTs afterCI, cChatItemId afterCI) ) -getLocalChat :: DB.Connection -> User -> Int64 -> ChatPagination -> Maybe Text -> ExceptT StoreError IO (Chat 'CTLocal, Maybe NavigationInfo) -getLocalChat db user folderId pagination search_ = do +getLocalChat :: DB.Connection -> User -> Int64 -> Maybe MsgContentTag -> ChatPagination -> Maybe Text -> ExceptT StoreError IO (Chat 'CTLocal, Maybe NavigationInfo) +getLocalChat db user folderId contentFilter pagination search_ = do let search = fromMaybe "" search_ nf <- getNoteFolder db user folderId case pagination of - CPLast count -> liftIO $ (,Nothing) <$> getLocalChatLast_ db user nf count search - CPAfter afterId count -> (,Nothing) <$> getLocalChatAfter_ db user nf afterId count search - CPBefore beforeId count -> (,Nothing) <$> getLocalChatBefore_ db user nf beforeId count search - CPAround aroundId count -> getLocalChatAround_ db user nf aroundId count search + CPLast count -> (,Nothing) <$> getLocalChatLast_ db user nf contentFilter count search + CPAfter afterId count -> (,Nothing) <$> getLocalChatAfter_ db user nf contentFilter afterId count search + CPBefore beforeId count -> (,Nothing) <$> getLocalChatBefore_ db user nf contentFilter beforeId count search + CPAround aroundId count -> getLocalChatAround_ db user nf contentFilter aroundId count search CPInitial count -> do unless (T.null search) $ throwError $ SEInternalError "initial chat pagination doesn't support search" - getLocalChatInitial_ db user nf count + getLocalChatInitial_ db user nf contentFilter count -getLocalChatLast_ :: DB.Connection -> User -> NoteFolder -> Int -> Text -> IO (Chat 'CTLocal) -getLocalChatLast_ db user nf count search = do - ciIds <- getLocalChatItemIdsLast_ db user nf count search - ts <- getCurrentTime - cis <- mapM (safeGetLocalItem db user nf ts) ciIds - pure $ Chat (LocalChat nf) (reverse cis) emptyChatStats - -getLocalChatItemIdsLast_ :: DB.Connection -> User -> NoteFolder -> Int -> Text -> IO [ChatItemId] -getLocalChatItemIdsLast_ db User {userId} NoteFolder {noteFolderId} count search = - map fromOnly - <$> DB.query - db - [sql| - SELECT chat_item_id - FROM chat_items - WHERE user_id = ? AND note_folder_id = ? AND LOWER(item_text) LIKE '%' || LOWER(?) || '%' - ORDER BY created_at DESC, chat_item_id DESC - LIMIT ? - |] - (userId, noteFolderId, search, count) +getLocalChatLast_ :: DB.Connection -> User -> NoteFolder -> Maybe MsgContentTag -> Int -> Text -> ExceptT StoreError IO (Chat 'CTLocal) +getLocalChatLast_ db user nf contentFilter count search = do + let cInfo = LocalChat nf + ciIds <- getChatItemIDs db user cInfo contentFilter CRLast count search + ts <- liftIO getCurrentTime + cis <- liftIO $ mapM (safeGetLocalItem db user nf ts) ciIds + pure $ Chat cInfo (reverse cis) emptyChatStats safeGetLocalItem :: DB.Connection -> User -> NoteFolder -> UTCTime -> ChatItemId -> IO (CChatItem 'CTLocal) safeGetLocalItem db user NoteFolder {noteFolderId} currentTs itemId = @@ -1804,81 +1785,57 @@ safeToLocalItem currentTs itemId = \case file = Nothing } -getLocalChatAfter_ :: DB.Connection -> User -> NoteFolder -> ChatItemId -> Int -> Text -> ExceptT StoreError IO (Chat 'CTLocal) -getLocalChatAfter_ db user nf@NoteFolder {noteFolderId} afterId count search = do +getLocalChatAfter_ :: DB.Connection -> User -> NoteFolder -> Maybe MsgContentTag -> ChatItemId -> Int -> Text -> ExceptT StoreError IO (Chat 'CTLocal) +getLocalChatAfter_ db user nf@NoteFolder {noteFolderId} contentFilter afterId count search = do afterCI <- getLocalChatItem db user noteFolderId afterId - ciIds <- liftIO $ getLocalCIsAfter_ db user nf afterCI count search + let cInfo = LocalChat nf + range = CRAfter (ciCreatedAt afterCI) (cChatItemId afterCI) + ciIds <- getChatItemIDs db user cInfo contentFilter range count search ts <- liftIO getCurrentTime cis <- liftIO $ mapM (safeGetLocalItem db user nf ts) ciIds - pure $ Chat (LocalChat nf) cis emptyChatStats + pure $ Chat cInfo cis emptyChatStats -getLocalCIsAfter_ :: DB.Connection -> User -> NoteFolder -> CChatItem 'CTLocal -> Int -> Text -> IO [ChatItemId] -getLocalCIsAfter_ db User {userId} NoteFolder {noteFolderId} afterCI count search = - map fromOnly - <$> DB.query - db - [sql| - SELECT chat_item_id - FROM chat_items - WHERE user_id = ? AND note_folder_id = ? AND LOWER(item_text) LIKE '%' || LOWER(?) || '%' - AND (created_at > ? OR (created_at = ? AND chat_item_id > ?)) - ORDER BY created_at ASC, chat_item_id ASC - LIMIT ? - |] - (userId, noteFolderId, search, ciCreatedAt afterCI, ciCreatedAt afterCI, cChatItemId afterCI, count) - -getLocalChatBefore_ :: DB.Connection -> User -> NoteFolder -> ChatItemId -> Int -> Text -> ExceptT StoreError IO (Chat 'CTLocal) -getLocalChatBefore_ db user nf@NoteFolder {noteFolderId} beforeId count search = do +getLocalChatBefore_ :: DB.Connection -> User -> NoteFolder -> Maybe MsgContentTag -> ChatItemId -> Int -> Text -> ExceptT StoreError IO (Chat 'CTLocal) +getLocalChatBefore_ db user nf@NoteFolder {noteFolderId} contentFilter beforeId count search = do beforeCI <- getLocalChatItem db user noteFolderId beforeId - ciIds <- liftIO $ getLocalCIsBefore_ db user nf beforeCI count search + let cInfo = LocalChat nf + range = CRBefore (ciCreatedAt beforeCI) (cChatItemId beforeCI) + ciIds <- getChatItemIDs db user cInfo contentFilter range count search ts <- liftIO getCurrentTime cis <- liftIO $ mapM (safeGetLocalItem db user nf ts) ciIds - pure $ Chat (LocalChat nf) (reverse cis) emptyChatStats + pure $ Chat cInfo (reverse cis) emptyChatStats -getLocalCIsBefore_ :: DB.Connection -> User -> NoteFolder -> CChatItem 'CTLocal -> Int -> Text -> IO [ChatItemId] -getLocalCIsBefore_ db User {userId} NoteFolder {noteFolderId} beforeCI count search = - map fromOnly - <$> DB.query - db - [sql| - SELECT chat_item_id - FROM chat_items - WHERE user_id = ? AND note_folder_id = ? AND LOWER(item_text) LIKE '%' || LOWER(?) || '%' - AND (created_at < ? OR (created_at = ? AND chat_item_id < ?)) - ORDER BY created_at DESC, chat_item_id DESC - LIMIT ? - |] - (userId, noteFolderId, search, ciCreatedAt beforeCI, ciCreatedAt beforeCI, cChatItemId beforeCI, count) - -getLocalChatAround_ :: DB.Connection -> User -> NoteFolder -> ChatItemId -> Int -> Text -> ExceptT StoreError IO (Chat 'CTLocal, Maybe NavigationInfo) -getLocalChatAround_ db user nf aroundId count search = do +getLocalChatAround_ :: DB.Connection -> User -> NoteFolder -> Maybe MsgContentTag -> ChatItemId -> Int -> Text -> ExceptT StoreError IO (Chat 'CTLocal, Maybe NavigationInfo) +getLocalChatAround_ db user nf contentFilter aroundId count search = do stats <- liftIO $ getLocalStats_ db user nf - getLocalChatAround' db user nf aroundId count search stats + getLocalChatAround' db user nf contentFilter aroundId count search stats -getLocalChatAround' :: DB.Connection -> User -> NoteFolder -> ChatItemId -> Int -> Text -> ChatStats -> ExceptT StoreError IO (Chat 'CTLocal, Maybe NavigationInfo) -getLocalChatAround' db user nf@NoteFolder {noteFolderId} aroundId count search stats = do +getLocalChatAround' :: DB.Connection -> User -> NoteFolder -> Maybe MsgContentTag -> ChatItemId -> Int -> Text -> ChatStats -> ExceptT StoreError IO (Chat 'CTLocal, Maybe NavigationInfo) +getLocalChatAround' db user nf@NoteFolder {noteFolderId} contentFilter aroundId count search stats = do aroundCI <- getLocalChatItem db user noteFolderId aroundId - beforeIds <- liftIO $ getLocalCIsBefore_ db user nf aroundCI count search - afterIds <- liftIO $ getLocalCIsAfter_ db user nf aroundCI count search + let cInfo = LocalChat nf + range r = r (ciCreatedAt aroundCI) (cChatItemId aroundCI) + beforeIds <- getChatItemIDs db user cInfo contentFilter (range CRBefore) count search + afterIds <- getChatItemIDs db user cInfo contentFilter (range CRAfter) count search ts <- liftIO getCurrentTime beforeCIs <- liftIO $ mapM (safeGetLocalItem db user nf ts) beforeIds afterCIs <- liftIO $ mapM (safeGetLocalItem db user nf ts) afterIds let cis = reverse beforeCIs <> [aroundCI] <> afterCIs navInfo <- liftIO $ getNavInfo cis - pure (Chat (LocalChat nf) cis stats, Just navInfo) + pure (Chat cInfo cis stats, Just navInfo) where getNavInfo cis_ = case cis_ of [] -> pure $ NavigationInfo 0 0 cis -> getLocalNavInfo_ db user nf (last cis) -getLocalChatInitial_ :: DB.Connection -> User -> NoteFolder -> Int -> ExceptT StoreError IO (Chat 'CTLocal, Maybe NavigationInfo) -getLocalChatInitial_ db user nf count = do +getLocalChatInitial_ :: DB.Connection -> User -> NoteFolder -> Maybe MsgContentTag -> Int -> ExceptT StoreError IO (Chat 'CTLocal, Maybe NavigationInfo) +getLocalChatInitial_ db user nf contentFilter count = do liftIO (getLocalMinUnreadId_ db user nf) >>= \case Just minUnreadItemId -> do unreadCount <- liftIO $ getLocalUnreadCount_ db user nf let stats = emptyChatStats {unreadCount, minUnreadItemId} - getLocalChatAround' db user nf minUnreadItemId count "" stats - Nothing -> liftIO $ (,Just $ NavigationInfo 0 0) <$> getLocalChatLast_ db user nf count "" + getLocalChatAround' db user nf contentFilter minUnreadItemId count "" stats + Nothing -> (,Just $ NavigationInfo 0 0) <$> getLocalChatLast_ db user nf contentFilter count "" getLocalStats_ :: DB.Connection -> User -> NoteFolder -> IO ChatStats getLocalStats_ db user nf = do diff --git a/src/Simplex/Chat/Store/Postgres/Migrations.hs b/src/Simplex/Chat/Store/Postgres/Migrations.hs index ef6d96bd45..3f8cc5b64b 100644 --- a/src/Simplex/Chat/Store/Postgres/Migrations.hs +++ b/src/Simplex/Chat/Store/Postgres/Migrations.hs @@ -24,6 +24,7 @@ import Simplex.Chat.Store.Postgres.Migrations.M20251017_chat_tags_cascade import Simplex.Chat.Store.Postgres.Migrations.M20251117_member_relations_vector import Simplex.Chat.Store.Postgres.Migrations.M20251128_migrate_member_relations import Simplex.Chat.Store.Postgres.Migrations.M20251230_strict_tables +import Simplex.Chat.Store.Postgres.Migrations.M20260108_chat_indices import Simplex.Messaging.Agent.Store.Shared (Migration (..)) schemaMigrations :: [(String, Text, Maybe Text)] @@ -47,7 +48,8 @@ schemaMigrations = ("20251017_chat_tags_cascade", m20251017_chat_tags_cascade, Just down_m20251017_chat_tags_cascade), ("20251117_member_relations_vector", m20251117_member_relations_vector, Just down_m20251117_member_relations_vector), ("20251128_migrate_member_relations", m20251128_migrate_member_relations, Just down_m20251128_migrate_member_relations), - ("20251230_strict_tables", m20251230_strict_tables, Just down_m20251230_strict_tables) + ("20251230_strict_tables", m20251230_strict_tables, Just down_m20251230_strict_tables), + ("20260108_chat_indices", m20260108_chat_indices, Just down_m20260108_chat_indices) ] -- | The list of migrations in ascending order by date diff --git a/src/Simplex/Chat/Store/Postgres/Migrations/M20260108_chat_indices.hs b/src/Simplex/Chat/Store/Postgres/Migrations/M20260108_chat_indices.hs new file mode 100644 index 0000000000..e7104dfbe3 --- /dev/null +++ b/src/Simplex/Chat/Store/Postgres/Migrations/M20260108_chat_indices.hs @@ -0,0 +1,33 @@ +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Store.Postgres.Migrations.M20260108_chat_indices where + +import Data.Text (Text) +import Text.RawString.QQ (r) + +m20260108_chat_indices :: Text +m20260108_chat_indices = + [r| +CREATE INDEX idx_chat_items_contacts_msg_content_tag_created_at ON chat_items( + user_id, + contact_id, + msg_content_tag, + created_at +); + +CREATE INDEX idx_chat_items_note_folder_msg_content_tag_created_at ON chat_items( + user_id, + note_folder_id, + msg_content_tag, + created_at +); +|] + +down_m20260108_chat_indices :: Text +down_m20260108_chat_indices = + [r| +DROP INDEX idx_chat_items_contacts_msg_content_tag_created_at; + +DROP INDEX idx_chat_items_note_folder_msg_content_tag_created_at; +|] diff --git a/src/Simplex/Chat/Store/Postgres/Migrations/chat_schema.sql b/src/Simplex/Chat/Store/Postgres/Migrations/chat_schema.sql index 238a6bfdbb..c37d1360d6 100644 --- a/src/Simplex/Chat/Store/Postgres/Migrations/chat_schema.sql +++ b/src/Simplex/Chat/Store/Postgres/Migrations/chat_schema.sql @@ -1813,6 +1813,10 @@ CREATE INDEX idx_chat_items_contacts_created_at ON test_chat_schema.chat_items U +CREATE INDEX idx_chat_items_contacts_msg_content_tag_created_at ON test_chat_schema.chat_items USING btree (user_id, contact_id, msg_content_tag, created_at); + + + CREATE UNIQUE INDEX idx_chat_items_direct_shared_msg_id ON test_chat_schema.chat_items USING btree (user_id, contact_id, shared_msg_id); @@ -1897,6 +1901,10 @@ CREATE INDEX idx_chat_items_item_status ON test_chat_schema.chat_items USING btr +CREATE INDEX idx_chat_items_note_folder_msg_content_tag_created_at ON test_chat_schema.chat_items USING btree (user_id, note_folder_id, msg_content_tag, created_at); + + + CREATE INDEX idx_chat_items_notes ON test_chat_schema.chat_items USING btree (user_id, note_folder_id, item_status, created_at); diff --git a/src/Simplex/Chat/Store/SQLite/Migrations.hs b/src/Simplex/Chat/Store/SQLite/Migrations.hs index f740c5654f..fc0da3c04a 100644 --- a/src/Simplex/Chat/Store/SQLite/Migrations.hs +++ b/src/Simplex/Chat/Store/SQLite/Migrations.hs @@ -147,6 +147,7 @@ import Simplex.Chat.Store.SQLite.Migrations.M20251017_chat_tags_cascade import Simplex.Chat.Store.SQLite.Migrations.M20251117_member_relations_vector import Simplex.Chat.Store.SQLite.Migrations.M20251128_migrate_member_relations import Simplex.Chat.Store.SQLite.Migrations.M20251230_strict_tables +import Simplex.Chat.Store.SQLite.Migrations.M20260108_chat_indices import Simplex.Messaging.Agent.Store.Shared (Migration (..)) schemaMigrations :: [(String, Query, Maybe Query)] @@ -293,7 +294,8 @@ schemaMigrations = ("20251017_chat_tags_cascade", m20251017_chat_tags_cascade, Just down_m20251017_chat_tags_cascade), ("20251117_member_relations_vector", m20251117_member_relations_vector, Just down_m20251117_member_relations_vector), ("20251128_migrate_member_relations", m20251128_migrate_member_relations, Just down_m20251128_migrate_member_relations), - ("20251230_strict_tables", m20251230_strict_tables, Just down_m20251230_strict_tables) + ("20251230_strict_tables", m20251230_strict_tables, Just down_m20251230_strict_tables), + ("20260108_chat_indices", m20260108_chat_indices, Just down_m20260108_chat_indices) ] -- | The list of migrations in ascending order by date diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/M20260108_chat_indices.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20260108_chat_indices.hs new file mode 100644 index 0000000000..b64bef72cf --- /dev/null +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20260108_chat_indices.hs @@ -0,0 +1,32 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Store.SQLite.Migrations.M20260108_chat_indices where + +import Database.SQLite.Simple (Query) +import Database.SQLite.Simple.QQ (sql) + +m20260108_chat_indices :: Query +m20260108_chat_indices = + [sql| +CREATE INDEX idx_chat_items_contacts_msg_content_tag_created_at ON chat_items( + user_id, + contact_id, + msg_content_tag, + created_at +); + +CREATE INDEX idx_chat_items_note_folder_msg_content_tag_created_at ON chat_items( + user_id, + note_folder_id, + msg_content_tag, + created_at +); +|] + +down_m20260108_chat_indices :: Query +down_m20260108_chat_indices = + [sql| +DROP INDEX idx_chat_items_contacts_msg_content_tag_created_at; + +DROP INDEX idx_chat_items_note_folder_msg_content_tag_created_at; +|] diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/agent_query_plans.txt b/src/Simplex/Chat/Store/SQLite/Migrations/agent_query_plans.txt index 6d3f69d5fa..1b881bd446 100644 --- a/src/Simplex/Chat/Store/SQLite/Migrations/agent_query_plans.txt +++ b/src/Simplex/Chat/Store/SQLite/Migrations/agent_query_plans.txt @@ -59,6 +59,16 @@ Query: Plan: SEARCH commands USING INDEX idx_commands_server_commands (host=? AND port=?) +Query: + SELECT rcpt_status, snd_message_body_id FROM snd_messages + WHERE NOT EXISTS (SELECT 1 FROM snd_message_deliveries WHERE conn_id = ? AND internal_id = ? AND failed = 0) + AND conn_id = ? AND internal_id = ? + +Plan: +SEARCH snd_messages USING PRIMARY KEY (conn_id=?) +SCALAR SUBQUERY 1 +SEARCH snd_message_deliveries USING COVERING INDEX idx_snd_message_deliveries_expired (conn_id=?) + Query: SELECT rcv_file_chunk_id, chunk_no, chunk_size, digest, tmp_path FROM rcv_file_chunks @@ -69,6 +79,14 @@ Plan: SEARCH rcv_file_chunks USING INDEX idx_rcv_file_chunks_rcv_file_id (rcv_file_id=?) USE TEMP B-TREE FOR ORDER BY +Query: + SELECT rcv_file_entity_id, user_id, size, digest, key, nonce, chunk_size, prefix_path, tmp_path, save_path, save_file_key, save_file_nonce, status, deleted, redirect_id, redirect_entity_id, redirect_size, redirect_digest + FROM rcv_files + WHERE rcv_file_id = ? + +Plan: +SEARCH rcv_files USING INTEGER PRIMARY KEY (rowid=?) + Query: SELECT snd_file_chunk_id, chunk_no, chunk_offset, chunk_size, digest FROM snd_file_chunks @@ -77,6 +95,14 @@ Query: Plan: SEARCH snd_file_chunks USING INDEX idx_snd_file_chunks_snd_file_id (snd_file_id=?) +Query: + SELECT snd_file_entity_id, user_id, path, src_file_key, src_file_nonce, num_recipients, digest, prefix_path, key, nonce, status, deleted, redirect_size, redirect_digest + FROM snd_files + WHERE snd_file_id = ? + +Plan: +SEARCH snd_files USING INTEGER PRIMARY KEY (rowid=?) + Query: DELETE FROM snd_message_bodies WHERE NOT EXISTS (SELECT 1 FROM snd_messages WHERE snd_message_body_id = ?) @@ -201,24 +227,6 @@ SEARCH c USING INTEGER PRIMARY KEY (rowid=?) SEARCH f USING INTEGER PRIMARY KEY (rowid=?) USE TEMP B-TREE FOR ORDER BY -Query: - SELECT rcpt_status, snd_message_body_id FROM snd_messages - WHERE NOT EXISTS (SELECT 1 FROM snd_message_deliveries WHERE conn_id = ? AND internal_id = ? AND failed = 0) - AND conn_id = ? AND internal_id = ? - -Plan: -SEARCH snd_messages USING PRIMARY KEY (conn_id=?) -SCALAR SUBQUERY 1 -SEARCH snd_message_deliveries USING COVERING INDEX idx_snd_message_deliveries_expired (conn_id=?) - -Query: - SELECT rcv_file_entity_id, user_id, size, digest, key, nonce, chunk_size, prefix_path, tmp_path, save_path, save_file_key, save_file_nonce, status, deleted, redirect_id, redirect_entity_id, redirect_size, redirect_digest - FROM rcv_files - WHERE rcv_file_id = ? - -Plan: -SEARCH rcv_files USING INTEGER PRIMARY KEY (rowid=?) - Query: SELECT rcv_file_id FROM rcv_files @@ -230,14 +238,6 @@ Plan: SEARCH rcv_files USING INDEX idx_rcv_files_status_created_at (status=? AND created_at>?) USE TEMP B-TREE FOR ORDER BY -Query: - SELECT snd_file_entity_id, user_id, path, src_file_key, src_file_nonce, num_recipients, digest, prefix_path, key, nonce, status, deleted, redirect_size, redirect_digest - FROM snd_files - WHERE snd_file_id = ? - -Plan: -SEARCH snd_files USING INTEGER PRIMARY KEY (rowid=?) - Query: SELECT snd_file_id FROM snd_files @@ -257,6 +257,22 @@ Query: Plan: SEARCH messages USING PRIMARY KEY (conn_id=?) +Query: + SELECT last_internal_msg_id, last_internal_rcv_msg_id, last_external_snd_msg_id, last_rcv_msg_hash + FROM connections + WHERE conn_id = ? + +Plan: +SEARCH connections USING PRIMARY KEY (conn_id=?) + +Query: + SELECT last_internal_msg_id, last_internal_snd_msg_id, last_snd_msg_hash + FROM connections + WHERE conn_id = ? + +Plan: +SEARCH connections USING PRIMARY KEY (conn_id=?) + Query: SELECT user_id FROM users u WHERE u.user_id = ? @@ -268,6 +284,15 @@ SEARCH u USING INTEGER PRIMARY KEY (rowid=?) CORRELATED SCALAR SUBQUERY 1 SEARCH c USING COVERING INDEX idx_connections_user (user_id=?) +Query: + SELECT user_id, conn_id, conn_mode, smp_agent_version, enable_ntfs, + last_external_snd_msg_id, deleted, ratchet_sync_state, pq_support + FROM connections + WHERE conn_id = ? AND deleted = ? + +Plan: +SEARCH connections USING PRIMARY KEY (conn_id=?) + Query: INSERT INTO conn_confirmations (confirmation_id, conn_id, sender_key, e2e_snd_pub_key, ratchet_state, sender_conn_info, smp_reply_queues, smp_client_version, accepted) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 0); @@ -287,6 +312,18 @@ Query: Plan: +Query: + INSERT INTO xftp_servers (xftp_host, xftp_port, xftp_key_hash) + VALUES (?, ?, ?) + ON CONFLICT (xftp_host, xftp_port, xftp_key_hash) + DO UPDATE SET xftp_host = EXCLUDED.xftp_host + RETURNING xftp_server_id + +Plan: +SEARCH deleted_snd_chunk_replicas USING COVERING INDEX idx_deleted_snd_chunk_replicas_xftp_server_id (xftp_server_id=?) +SEARCH snd_file_chunk_replicas USING COVERING INDEX idx_snd_file_chunk_replicas_xftp_server_id (xftp_server_id=?) +SEARCH rcv_file_chunk_replicas USING COVERING INDEX idx_rcv_file_chunk_replicas_xftp_server_id (xftp_server_id=?) + Query: SELECT r.internal_id, m.internal_ts, r.broker_id, r.broker_ts, r.external_snd_id, r.integrity, r.internal_hash, @@ -445,22 +482,6 @@ Query: Plan: SEARCH conn_invitations USING PRIMARY KEY (invitation_id=?) -Query: - SELECT last_internal_msg_id, last_internal_rcv_msg_id, last_external_snd_msg_id, last_rcv_msg_hash - FROM connections - WHERE conn_id = ? - -Plan: -SEARCH connections USING PRIMARY KEY (conn_id=?) - -Query: - SELECT last_internal_msg_id, last_internal_snd_msg_id, last_snd_msg_hash - FROM connections - WHERE conn_id = ? - -Plan: -SEARCH connections USING PRIMARY KEY (conn_id=?) - Query: SELECT link_id, snd_private_key FROM inv_short_links @@ -497,15 +518,6 @@ Plan: SEARCH s USING PRIMARY KEY (conn_id=? AND internal_snd_id=?) SEARCH m USING PRIMARY KEY (conn_id=? AND internal_id=?) -Query: - SELECT user_id, conn_id, conn_mode, smp_agent_version, enable_ntfs, - last_external_snd_msg_id, deleted, ratchet_sync_state, pq_support - FROM connections - WHERE conn_id = ? AND deleted = ? - -Plan: -SEARCH connections USING PRIMARY KEY (conn_id=?) - Query: DELETE FROM conn_confirmations WHERE conn_id = ? @@ -1027,8 +1039,13 @@ Plan: Query: INSERT INTO rcv_files (rcv_file_entity_id, user_id, size, digest, key, nonce, chunk_size, prefix_path, tmp_path, save_path, save_file_key, save_file_nonce, status, redirect_id, redirect_entity_id, redirect_digest, redirect_size, approved_relays) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) Plan: -Query: INSERT INTO servers (host, port, key_hash) VALUES (?,?,?) +Query: INSERT INTO servers (host, port, key_hash) VALUES (?,?,?) ON CONFLICT (host, port) DO NOTHING RETURNING 1 Plan: +SEARCH inv_short_links USING COVERING INDEX idx_inv_short_links_link_id (host=? AND port=?) +SEARCH commands USING COVERING INDEX idx_commands_server_commands (host=? AND port=?) +SEARCH ntf_subscriptions USING COVERING INDEX idx_ntf_subscriptions_smp_host_smp_port (smp_host=? AND smp_port=?) +SEARCH snd_queues USING COVERING INDEX idx_snd_queues_host_port (host=? AND port=?) +SEARCH rcv_queues USING COVERING INDEX idx_rcv_queues_link_id (host=? AND port=?) Query: INSERT INTO skipped_messages (conn_id, header_key, msg_n, msg_key) VALUES (?, ?, ?, ?) Plan: @@ -1052,9 +1069,6 @@ Plan: Query: INSERT INTO users DEFAULT VALUES Plan: -Query: INSERT INTO xftp_servers (xftp_host, xftp_port, xftp_key_hash) VALUES (?,?,?) -Plan: - Query: SELECT 1 FROM encrypted_rcv_message_hashes WHERE conn_id = ? AND hash = ? LIMIT 1 Plan: SEARCH encrypted_rcv_message_hashes USING COVERING INDEX idx_encrypted_rcv_message_hashes_hash (conn_id=? AND hash=?) @@ -1159,10 +1173,6 @@ Query: SELECT x3dh_priv_key_1, x3dh_priv_key_2, pq_priv_kem FROM ratchets WHERE Plan: SEARCH ratchets USING PRIMARY KEY (conn_id=?) -Query: SELECT xftp_server_id FROM xftp_servers WHERE xftp_host = ? AND xftp_port = ? AND xftp_key_hash = ? -Plan: -SEARCH xftp_servers USING COVERING INDEX sqlite_autoindex_xftp_servers_1 (xftp_host=? AND xftp_port=? AND xftp_key_hash=?) - Query: UPDATE connections SET deleted = ? WHERE conn_id = ? Plan: SEARCH connections USING PRIMARY KEY (conn_id=?) diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt b/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt index 69fa665911..30751ee535 100644 --- a/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt +++ b/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt @@ -736,6 +736,26 @@ Query: Plan: SEARCH messages USING INDEX idx_messages_group_id_shared_msg_id (group_id=? AND shared_msg_id=?) +Query: + SELECT chat_item_id + FROM chat_items + WHERE user_id = ? AND contact_id = ? + ORDER BY created_at DESC, chat_item_id DESC + LIMIT 1 + +Plan: +SEARCH chat_items USING COVERING INDEX idx_chat_items_contacts_created_at (user_id=? AND contact_id=?) + +Query: + SELECT chat_item_id + FROM chat_items + WHERE user_id = ? AND group_id = ? AND group_member_id = ? + ORDER BY item_ts DESC, chat_item_id DESC + LIMIT 1 + +Plan: +SEARCH chat_items USING INDEX idx_chat_items_groups_item_ts (user_id=? AND group_id=?) + Query: SELECT chat_item_id, contact_id, group_id, group_scope_tag, group_scope_group_member_id, note_folder_id FROM chat_items @@ -745,7 +765,7 @@ Query: LIMIT ? Plan: -SEARCH chat_items USING INDEX idx_chat_items_group_scope_item_ts (user_id=?) +SEARCH chat_items USING INDEX idx_chat_items_note_folder_msg_content_tag_created_at (user_id=?) USE TEMP B-TREE FOR ORDER BY Query: @@ -756,7 +776,7 @@ Query: LIMIT ? Plan: -SEARCH chat_items USING INDEX idx_chat_items_group_scope_item_ts (user_id=?) +SEARCH chat_items USING INDEX idx_chat_items_note_folder_msg_content_tag_created_at (user_id=?) USE TEMP B-TREE FOR ORDER BY Query: @@ -1225,26 +1245,6 @@ Plan: SEARCH c USING INDEX idx_connections_contact_id (contact_id=?) SEARCH ct USING INTEGER PRIMARY KEY (rowid=?) -Query: - SELECT chat_item_id - FROM chat_items - WHERE user_id = ? AND contact_id = ? - ORDER BY created_at DESC, chat_item_id DESC - LIMIT 1 - -Plan: -SEARCH chat_items USING COVERING INDEX idx_chat_items_contacts_created_at (user_id=? AND contact_id=?) - -Query: - SELECT chat_item_id - FROM chat_items - WHERE user_id = ? AND group_id = ? AND group_member_id = ? - ORDER BY item_ts DESC, chat_item_id DESC - LIMIT 1 - -Plan: -SEARCH chat_items USING INDEX idx_chat_items_groups_item_ts (user_id=? AND group_id=?) - Query: SELECT chat_item_id FROM chat_items @@ -1292,7 +1292,7 @@ Query: LIMIT ? Plan: -SEARCH chat_items USING INDEX idx_chat_items_group_scope_item_ts (user_id=?) +SEARCH chat_items USING INDEX idx_chat_items_note_folder_msg_content_tag_created_at (user_id=?) USE TEMP B-TREE FOR ORDER BY Query: @@ -3131,38 +3131,6 @@ SEARCH chat_item_messages USING INDEX sqlite_autoindex_chat_item_messages_1 (mes LIST SUBQUERY 1 SEARCH msg_deliveries USING INDEX idx_msg_deliveries_agent_msg_id (connection_id=? AND agent_msg_id=?) -Query: - SELECT chat_item_id - FROM chat_items - WHERE user_id = ? AND contact_id = ? AND LOWER(item_text) LIKE '%' || LOWER(?) || '%' - AND (created_at < ? OR (created_at = ? AND chat_item_id < ?)) - ORDER BY created_at DESC, chat_item_id DESC - LIMIT ? - -Plan: -SEARCH chat_items USING INDEX idx_chat_items_contacts_created_at (user_id=? AND contact_id=?) - -Query: - SELECT chat_item_id - FROM chat_items - WHERE user_id = ? AND contact_id = ? AND LOWER(item_text) LIKE '%' || LOWER(?) || '%' - AND (created_at > ? OR (created_at = ? AND chat_item_id > ?)) - ORDER BY created_at ASC, chat_item_id ASC - LIMIT ? - -Plan: -SEARCH chat_items USING INDEX idx_chat_items_contacts_created_at (user_id=? AND contact_id=?) - -Query: - SELECT chat_item_id - FROM chat_items - WHERE user_id = ? AND contact_id = ? AND LOWER(item_text) LIKE '%' || LOWER(?) || '%' - ORDER BY created_at DESC, chat_item_id DESC - LIMIT ? - -Plan: -SEARCH chat_items USING INDEX idx_chat_items_contacts_created_at (user_id=? AND contact_id=?) - Query: SELECT chat_item_id FROM chat_items @@ -3213,38 +3181,6 @@ Query: Plan: SEARCH chat_items USING INDEX idx_chat_items_group_id (group_id=?) -Query: - SELECT chat_item_id - FROM chat_items - WHERE user_id = ? AND note_folder_id = ? AND LOWER(item_text) LIKE '%' || LOWER(?) || '%' - AND (created_at < ? OR (created_at = ? AND chat_item_id < ?)) - ORDER BY created_at DESC, chat_item_id DESC - LIMIT ? - -Plan: -SEARCH chat_items USING INDEX idx_chat_items_notes_created_at (user_id=? AND note_folder_id=?) - -Query: - SELECT chat_item_id - FROM chat_items - WHERE user_id = ? AND note_folder_id = ? AND LOWER(item_text) LIKE '%' || LOWER(?) || '%' - AND (created_at > ? OR (created_at = ? AND chat_item_id > ?)) - ORDER BY created_at ASC, chat_item_id ASC - LIMIT ? - -Plan: -SEARCH chat_items USING INDEX idx_chat_items_notes_created_at (user_id=? AND note_folder_id=?) - -Query: - SELECT chat_item_id - FROM chat_items - WHERE user_id = ? AND note_folder_id = ? AND LOWER(item_text) LIKE '%' || LOWER(?) || '%' - ORDER BY created_at DESC, chat_item_id DESC - LIMIT ? - -Plan: -SEARCH chat_items USING INDEX idx_chat_items_notes_created_at (user_id=? AND note_folder_id=?) - Query: SELECT chat_item_id FROM chat_items @@ -5205,7 +5141,7 @@ Query: JOIN files f ON f.chat_item_id = i.chat_item_id WHERE i.user_id = ? Plan: -SEARCH i USING COVERING INDEX idx_chat_items_group_scope_item_ts (user_id=?) +SEARCH i USING COVERING INDEX idx_chat_items_note_folder_msg_content_tag_created_at (user_id=?) SEARCH f USING INDEX idx_files_chat_item_id (chat_item_id=?) Query: @@ -5475,7 +5411,25 @@ Query: Plan: SEARCH user_contact_links USING INDEX sqlite_autoindex_user_contact_links_1 (user_id=?) -Query: SELECT chat_item_id FROM chat_items WHERE (( user_id = ? AND group_id = ? AND group_scope_tag IS NULL AND group_scope_group_member_id IS NULL AND item_ts < ? ) OR ( user_id = ? AND group_id = ? AND group_scope_tag IS NULL AND group_scope_group_member_id IS NULL AND item_ts = ? AND chat_item_id < ? )) ORDER BY item_ts DESC, chat_item_id DESC LIMIT ? +Query: SELECT chat_item_id FROM chat_items WHERE (( user_id = ? AND contact_id = ? AND created_at < ?) OR ( user_id = ? AND contact_id = ? AND created_at = ? AND chat_item_id < ?)) ORDER BY created_at DESC, chat_item_id DESC LIMIT ? +Plan: +MULTI-INDEX OR +INDEX 1 +SEARCH chat_items USING COVERING INDEX idx_chat_items_contacts_created_at (user_id=? AND contact_id=? AND created_at ?) OR ( user_id = ? AND contact_id = ? AND created_at = ? AND chat_item_id > ?)) ORDER BY created_at ASC, chat_item_id ASC LIMIT ? +Plan: +MULTI-INDEX OR +INDEX 1 +SEARCH chat_items USING COVERING INDEX idx_chat_items_contacts_created_at (user_id=? AND contact_id=? AND created_at>?) +INDEX 2 +SEARCH chat_items USING COVERING INDEX idx_chat_items_contacts_created_at (user_id=? AND contact_id=? AND created_at=? AND rowid>?) +USE TEMP B-TREE FOR ORDER BY + +Query: SELECT chat_item_id FROM chat_items WHERE (( user_id = ? AND group_id = ? AND group_scope_tag IS NULL AND group_scope_group_member_id IS NULL AND item_ts < ?) OR ( user_id = ? AND group_id = ? AND group_scope_tag IS NULL AND group_scope_group_member_id IS NULL AND item_ts = ? AND chat_item_id < ?)) ORDER BY item_ts DESC, chat_item_id DESC LIMIT ? Plan: MULTI-INDEX OR INDEX 1 @@ -5484,7 +5438,7 @@ INDEX 2 SEARCH chat_items USING COVERING INDEX idx_chat_items_group_scope_item_ts (user_id=? AND group_id=? AND group_scope_tag=? AND group_scope_group_member_id=? AND item_ts=? AND rowid ? ) OR ( user_id = ? AND group_id = ? AND group_scope_tag IS NULL AND group_scope_group_member_id IS NULL AND item_ts = ? AND chat_item_id > ? )) ORDER BY item_ts ASC, chat_item_id ASC LIMIT ? +Query: SELECT chat_item_id FROM chat_items WHERE (( user_id = ? AND group_id = ? AND group_scope_tag IS NULL AND group_scope_group_member_id IS NULL AND item_ts > ?) OR ( user_id = ? AND group_id = ? AND group_scope_tag IS NULL AND group_scope_group_member_id IS NULL AND item_ts = ? AND chat_item_id > ?)) ORDER BY item_ts ASC, chat_item_id ASC LIMIT ? Plan: MULTI-INDEX OR INDEX 1 @@ -5493,11 +5447,37 @@ INDEX 2 SEARCH chat_items USING COVERING INDEX idx_chat_items_group_scope_item_ts (user_id=? AND group_id=? AND group_scope_tag=? AND group_scope_group_member_id=? AND item_ts=? AND rowid>?) USE TEMP B-TREE FOR ORDER BY -Query: SELECT chat_item_id FROM chat_items WHERE user_id = ? AND group_id = ? AND group_scope_tag = ? AND group_scope_group_member_id = ? ORDER BY item_ts DESC, chat_item_id DESC LIMIT ? +Query: SELECT chat_item_id FROM chat_items WHERE (( user_id = ? AND note_folder_id = ? AND created_at < ?) OR ( user_id = ? AND note_folder_id = ? AND created_at = ? AND chat_item_id < ?)) ORDER BY created_at DESC, chat_item_id DESC LIMIT ? Plan: -SEARCH chat_items USING COVERING INDEX idx_chat_items_group_scope_item_ts (user_id=? AND group_id=? AND group_scope_tag=? AND group_scope_group_member_id=?) +MULTI-INDEX OR +INDEX 1 +SEARCH chat_items USING COVERING INDEX idx_chat_items_notes_created_at (user_id=? AND note_folder_id=? AND created_at ?) OR ( user_id = ? AND note_folder_id = ? AND created_at = ? AND chat_item_id > ?)) ORDER BY created_at ASC, chat_item_id ASC LIMIT ? +Plan: +MULTI-INDEX OR +INDEX 1 +SEARCH chat_items USING COVERING INDEX idx_chat_items_notes_created_at (user_id=? AND note_folder_id=? AND created_at>?) +INDEX 2 +SEARCH chat_items USING COVERING INDEX idx_chat_items_notes_created_at (user_id=? AND note_folder_id=? AND created_at=? AND rowid>?) +USE TEMP B-TREE FOR ORDER BY + +Query: SELECT chat_item_id FROM chat_items WHERE user_id = ? AND contact_id = ? AND LOWER(item_text) LIKE '%' || LOWER(?) || '%' ORDER BY created_at DESC, chat_item_id DESC LIMIT ? +Plan: +SEARCH chat_items USING INDEX idx_chat_items_contacts_created_at (user_id=? AND contact_id=?) + +Query: SELECT chat_item_id FROM chat_items WHERE user_id = ? AND contact_id = ? AND msg_content_tag = ? ORDER BY created_at DESC, chat_item_id DESC LIMIT ? +Plan: +SEARCH chat_items USING COVERING INDEX idx_chat_items_contacts_msg_content_tag_created_at (user_id=? AND contact_id=? AND msg_content_tag=?) + +Query: SELECT chat_item_id FROM chat_items WHERE user_id = ? AND contact_id = ? ORDER BY created_at DESC, chat_item_id DESC LIMIT ? +Plan: +SEARCH chat_items USING COVERING INDEX idx_chat_items_contacts_created_at (user_id=? AND contact_id=?) + +Query: SELECT chat_item_id FROM chat_items WHERE user_id = ? AND group_id = ? AND group_scope_tag = ? AND group_scope_group_member_id IS NOT DISTINCT FROM ? ORDER BY item_ts DESC, chat_item_id DESC LIMIT ? Plan: SEARCH chat_items USING COVERING INDEX idx_chat_items_group_scope_item_ts (user_id=? AND group_id=? AND group_scope_tag=? AND group_scope_group_member_id=?) @@ -5513,6 +5493,18 @@ Query: SELECT chat_item_id FROM chat_items WHERE user_id = ? AND group_id = ? Plan: SEARCH chat_items USING COVERING INDEX idx_chat_items_groups_msg_content_tag_item_ts (user_id=? AND group_id=? AND msg_content_tag=?) +Query: SELECT chat_item_id FROM chat_items WHERE user_id = ? AND note_folder_id = ? AND LOWER(item_text) LIKE '%' || LOWER(?) || '%' ORDER BY created_at DESC, chat_item_id DESC LIMIT ? +Plan: +SEARCH chat_items USING INDEX idx_chat_items_notes_created_at (user_id=? AND note_folder_id=?) + +Query: SELECT chat_item_id FROM chat_items WHERE user_id = ? AND note_folder_id = ? AND msg_content_tag = ? ORDER BY created_at DESC, chat_item_id DESC LIMIT ? +Plan: +SEARCH chat_items USING COVERING INDEX idx_chat_items_note_folder_msg_content_tag_created_at (user_id=? AND note_folder_id=? AND msg_content_tag=?) + +Query: SELECT chat_item_id FROM chat_items WHERE user_id = ? AND note_folder_id = ? ORDER BY created_at DESC, chat_item_id DESC LIMIT ? +Plan: +SEARCH chat_items USING COVERING INDEX idx_chat_items_notes_created_at (user_id=? AND note_folder_id=?) + Query: CREATE TABLE temp_delete_members (contact_profile_id INTEGER, member_profile_id INTEGER, local_display_name TEXT) Error: SQLite3 returned ErrorError while attempting to perform prepare "explain query plan CREATE TABLE temp_delete_members (contact_profile_id INTEGER, member_profile_id INTEGER, local_display_name TEXT)": table temp_delete_members already exists @@ -5911,7 +5903,7 @@ SEARCH protocol_servers USING COVERING INDEX idx_smp_servers_user_id (user_id=?) SEARCH settings USING COVERING INDEX idx_settings_user_id (user_id=?) SEARCH commands USING COVERING INDEX idx_commands_user_id (user_id=?) SEARCH calls USING COVERING INDEX idx_calls_user_id (user_id=?) -SEARCH chat_items USING COVERING INDEX idx_chat_items_group_scope_item_ts (user_id=?) +SEARCH chat_items USING COVERING INDEX idx_chat_items_note_folder_msg_content_tag_created_at (user_id=?) SEARCH contact_requests USING COVERING INDEX sqlite_autoindex_contact_requests_2 (user_id=?) SEARCH user_contact_links USING COVERING INDEX sqlite_autoindex_user_contact_links_1 (user_id=?) SEARCH connections USING COVERING INDEX idx_connections_to_subscribe (user_id=?) @@ -6059,6 +6051,18 @@ Query: SELECT COUNT(1), COALESCE(SUM(user_mention), 0) FROM chat_items WHERE use Plan: SEARCH chat_items USING COVERING INDEX idx_chat_items_group_scope_stats_all (user_id=? AND group_id=? AND group_scope_tag=? AND group_scope_group_member_id=? AND item_status=?) +Query: SELECT DISTINCT msg_content_tag FROM chat_items WHERE user_id = ? AND contact_id = ? AND msg_content_tag IS NOT NULL ORDER BY msg_content_tag +Plan: +SEARCH chat_items USING COVERING INDEX idx_chat_items_contacts_msg_content_tag_created_at (user_id=? AND contact_id=? AND msg_content_tag>?) + +Query: SELECT DISTINCT msg_content_tag FROM chat_items WHERE user_id = ? AND group_id = ? AND group_scope_tag IS NULL AND group_scope_group_member_id IS NULL AND msg_content_tag IS NOT NULL ORDER BY msg_content_tag +Plan: +SEARCH chat_items USING INDEX idx_chat_items_groups_msg_content_tag_deleted (user_id=? AND group_id=? AND msg_content_tag>?) + +Query: SELECT DISTINCT msg_content_tag FROM chat_items WHERE user_id = ? AND note_folder_id = ? AND msg_content_tag IS NOT NULL ORDER BY msg_content_tag +Plan: +SEARCH chat_items USING COVERING INDEX idx_chat_items_note_folder_msg_content_tag_created_at (user_id=? AND note_folder_id=? AND msg_content_tag>?) + Query: SELECT EXISTS (SELECT 1 FROM chat_items WHERE user_id = ? AND contact_id = ?) Plan: SCAN CONSTANT ROW diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql b/src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql index fd4fa914d0..83b4c7dbe6 100644 --- a/src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql +++ b/src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql @@ -1187,6 +1187,18 @@ CREATE UNIQUE INDEX idx_group_members_group_id_index_in_group ON group_members( group_id, index_in_group ); +CREATE INDEX idx_chat_items_contacts_msg_content_tag_created_at ON chat_items( + user_id, + contact_id, + msg_content_tag, + created_at +); +CREATE INDEX idx_chat_items_note_folder_msg_content_tag_created_at ON chat_items( + user_id, + note_folder_id, + msg_content_tag, + created_at +); CREATE TRIGGER on_group_members_insert_update_summary AFTER INSERT ON group_members FOR EACH ROW diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs index b515123928..1926bfc3d6 100644 --- a/src/Simplex/Chat/View.hs +++ b/src/Simplex/Chat/View.hs @@ -122,6 +122,7 @@ chatResponseToView hu cfg@ChatConfig {logLevel, showReactions, testView} liveIte CRApiChats u chats -> ttyUser u $ if testView then testViewChats chats else [viewJSON chats] CRChats chats -> viewChats ts tz chats CRApiChat u chat _ -> ttyUser u $ if testView then testViewChat chat else [viewJSON chat] + CRChatContentTypes cts -> [plain $ "Chat content types: " <> T.intercalate ", " (map (safeDecodeUtf8 . strEncode) cts)] CRChatTags u tags -> ttyUser u $ [viewJSON tags] CRServerTestResult u srv testFailure -> ttyUser u $ viewServerTestResult srv testFailure CRServerOperatorConditions (ServerOperatorConditions ops _ ca) -> viewServerOperators ops ca diff --git a/tests/ChatTests/Files.hs b/tests/ChatTests/Files.hs index a71e7ae173..530b85fa91 100644 --- a/tests/ChatTests/Files.hs +++ b/tests/ChatTests/Files.hs @@ -68,7 +68,7 @@ runTestMessageWithFile :: HasCallStack => TestParams -> IO () runTestMessageWithFile = testChat2 aliceProfile bobProfile $ \alice bob -> withXFTPServer $ do connectUsers alice bob - alice ##> "/_send @2 json [{\"filePath\": \"./tests/fixtures/test.jpg\", \"msgContent\": {\"type\": \"text\", \"text\": \"hi, sending a file\"}}]" + alice ##> "/_send @2 json [{\"filePath\": \"./tests/fixtures/test.jpg\", \"msgContent\": {\"type\": \"file\", \"text\": \"hi, sending a file\"}}]" alice <# "@bob hi, sending a file" alice <# "/f @bob ./tests/fixtures/test.jpg" alice <## "use /fc 1 to cancel sending" @@ -83,12 +83,22 @@ runTestMessageWithFile = testChat2 aliceProfile bobProfile $ \alice bob -> withX "started receiving file 1 (test.jpg) from alice" ] bob <## "completed receiving file 1 (test.jpg) from alice" + bob #> "@alice received" + alice <# "bob> received" src <- B.readFile "./tests/fixtures/test.jpg" dest <- B.readFile "./tests/tmp/test.jpg" dest `shouldBe` src - alice #$> ("/_get chat @2 count=100", chatF, chatFeaturesF <> [((1, "hi, sending a file"), Just "./tests/fixtures/test.jpg")]) - bob #$> ("/_get chat @2 count=100", chatF, chatFeaturesF <> [((0, "hi, sending a file"), Just "./tests/tmp/test.jpg")]) + + alice #$> ("/_get chat @2 count=100", chatF, chatFeaturesF <> [((1, "hi, sending a file"), Just "./tests/fixtures/test.jpg"), ((0, "received"), Nothing)]) + alice ##> "/_get content types @2" + alice <## "Chat content types: file, text" + alice #$> ("/_get chat @2 content=file count=100", chatF, [((1, "hi, sending a file"), Just "./tests/fixtures/test.jpg")]) + + bob #$> ("/_get chat @2 count=100", chatF, chatFeaturesF <> [((0, "hi, sending a file"), Just "./tests/tmp/test.jpg"), ((1, "received"), Nothing)]) + bob ##> "/_get content types @2" + bob <## "Chat content types: file, text" + bob #$> ("/_get chat @2 content=file count=100", chatF, [((0, "hi, sending a file"), Just "./tests/tmp/test.jpg")]) testSendImage :: HasCallStack => TestParams -> IO () testSendImage = @@ -343,15 +353,33 @@ testGroupSendImage = "started receiving file 1 (test.jpg) from alice" ] cath <## "completed receiving file 1 (test.jpg) from alice" + threadDelay 1000000 + bob #> "#team received" + [alice, cath] *<# "#team bob> received" + threadDelay 1000000 + cath #> "#team received too" + [alice, bob] *<# "#team cath> received too" src <- B.readFile "./tests/fixtures/test.jpg" dest <- B.readFile "./tests/tmp/test.jpg" dest `shouldBe` src dest2 <- B.readFile "./tests/tmp/test_1.jpg" dest2 `shouldBe` src - alice #$> ("/_get chat #1 count=1", chatF, [((1, ""), Just "./tests/fixtures/test.jpg")]) - bob #$> ("/_get chat #1 count=1", chatF, [((0, ""), Just "./tests/tmp/test.jpg")]) - cath #$> ("/_get chat #1 count=1", chatF, [((0, ""), Just "./tests/tmp/test_1.jpg")]) + + alice #$> ("/_get chat #1 count=3", chatF, [((1, ""), Just "./tests/fixtures/test.jpg"), ((0, "received"), Nothing), ((0, "received too"), Nothing)]) + alice ##> "/_get content types #1" + alice <## "Chat content types: image, text" + alice #$> ("/_get chat #1 content=image count=100", chatF, [((1, ""), Just "./tests/fixtures/test.jpg")]) + + bob #$> ("/_get chat #1 count=3", chatF, [((0, ""), Just "./tests/tmp/test.jpg"), ((1, "received"), Nothing), ((0, "received too"), Nothing)]) + bob ##> "/_get content types #1" + bob <## "Chat content types: image, text" + bob #$> ("/_get chat #1 content=image count=100", chatF, [((0, ""), Just "./tests/tmp/test.jpg")]) + + cath #$> ("/_get chat #1 count=3", chatF, [((0, ""), Just "./tests/tmp/test_1.jpg"), ((0, "received"), Nothing), ((1, "received too"), Nothing)]) + cath ##> "/_get content types #1" + cath <## "Chat content types: image, text" + cath #$> ("/_get chat #1 content=image count=100", chatF, [((0, ""), Just "./tests/tmp/test_1.jpg")]) testGroupSendImageWithTextAndQuote :: HasCallStack => TestParams -> IO () testGroupSendImageWithTextAndQuote = diff --git a/tests/ChatTests/Local.hs b/tests/ChatTests/Local.hs index 985586816f..e4a4da5166 100644 --- a/tests/ChatTests/Local.hs +++ b/tests/ChatTests/Local.hs @@ -134,10 +134,13 @@ testFiles ps = withNewTestChat ps "alice" aliceProfile $ \alice -> do alice ##> "/tail" alice <# "* hi myself" alice <# "* file 1 (test.jpg)" + alice `send` "/* text note" + alice <# "* text note" - alice ##> "/_get chat *1 count=100" - r <- chatF <$> getTermLine alice - r `shouldBe` [((1, "hi myself"), Just "test.jpg")] + alice #$> ("/_get chat *1 count=100", chatF, [((1, "hi myself"), Just "test.jpg"), ((1, "text note"), Nothing)]) + alice ##> "/_get content types *1" + alice <## "Chat content types: image, text" + alice #$> ("/_get chat *1 content=image count=100", chatF, [((1, "hi myself"), Just "test.jpg")]) alice ##> "/fs 1" alice <## "bad chat command: not supported for local files" @@ -151,7 +154,7 @@ testFiles ps = withNewTestChat ps "alice" aliceProfile $ \alice -> do alice ##> "/_create *1 json [{\"filePath\": \"another_test.jpg\", \"msgContent\": {\"text\":\"\",\"type\":\"image\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\"}}]" alice <# "* file 2 (another_test.jpg)" - alice ##> "/_delete item *1 2 internal" + alice ##> "/_delete item *1 3 internal" alice <## "message deleted" doesFileExist stored2 `shouldReturn` False doesFileExist stored `shouldReturn` True From 72912f1be1c4bd18c8217386cb237f44662bf46c Mon Sep 17 00:00:00 2001 From: Evgeny Date: Fri, 9 Jan 2026 08:25:40 +0000 Subject: [PATCH 08/73] ui: api for media gallery content types (#6556) --- apps/ios/Shared/Model/AppAPITypes.swift | 12 +++++++++--- apps/ios/Shared/Model/SimpleXAPI.swift | 6 ++++++ apps/ios/SimpleXChat/ChatTypes.swift | 2 +- .../kotlin/chat/simplex/common/model/SimpleXAPI.kt | 14 ++++++++++++++ 4 files changed, 30 insertions(+), 4 deletions(-) diff --git a/apps/ios/Shared/Model/AppAPITypes.swift b/apps/ios/Shared/Model/AppAPITypes.swift index 193b675a57..e213f1c076 100644 --- a/apps/ios/Shared/Model/AppAPITypes.swift +++ b/apps/ios/Shared/Model/AppAPITypes.swift @@ -41,6 +41,7 @@ enum ChatCommand: ChatCmdProtocol { case apiGetChatTags(userId: Int64) case apiGetChats(userId: Int64) case apiGetChat(chatId: ChatId, scope: GroupChatScope?, contentTag: MsgContentTag?, pagination: ChatPagination, search: String) + case apiGetChatContentTypes(chatId: ChatId, scope: GroupChatScope?) case apiGetChatItemInfo(type: ChatType, id: Int64, scope: GroupChatScope?, itemId: Int64) case apiSendMessages(type: ChatType, id: Int64, scope: GroupChatScope?, live: Bool, ttl: Int?, composedMessages: [ComposedMessage]) case apiCreateChatTag(tag: ChatTagData) @@ -224,7 +225,8 @@ enum ChatCommand: ChatCmdProtocol { case let .apiGetChats(userId): return "/_get chats \(userId) pcc=on" case let .apiGetChat(chatId, scope, contentTag, pagination, search): let tag = contentTag != nil ? " content=\(contentTag!.rawValue)" : "" - return "/_get chat \(chatId)\(scopeRef(scope: scope))\(tag) \(pagination.cmdString)" + (search == "" ? "" : " search=\(search)") + return "/_get chat \(chatId)\(scopeRef(scope))\(tag) \(pagination.cmdString)" + (search == "" ? "" : " search=\(search)") + case let .apiGetChatContentTypes(chatId, scope): return "/_get content types \(chatId)\(scopeRef(scope))" case let .apiGetChatItemInfo(type, id, scope, itemId): return "/_get item info \(ref(type, id, scope: scope)) \(itemId)" case let .apiSendMessages(type, id, scope, live, ttl, composedMessages): let msgs = encodeJSON(composedMessages) @@ -417,6 +419,7 @@ enum ChatCommand: ChatCmdProtocol { case .apiGetChatTags: return "apiGetChatTags" case .apiGetChats: return "apiGetChats" case .apiGetChat: return "apiGetChat" + case .apiGetChatContentTypes: return "apiGetChatContentTypes" case .apiGetChatItemInfo: return "apiGetChatItemInfo" case .apiSendMessages: return "apiSendMessages" case .apiCreateChatTag: return "apiCreateChatTag" @@ -559,10 +562,10 @@ enum ChatCommand: ChatCmdProtocol { } func ref(_ type: ChatType, _ id: Int64, scope: GroupChatScope?) -> String { - "\(type.rawValue)\(id)\(scopeRef(scope: scope))" + "\(type.rawValue)\(id)\(scopeRef(scope))" } - func scopeRef(scope: GroupChatScope?) -> String { + func scopeRef(_ scope: GroupChatScope?) -> String { switch (scope) { case .none: "" case let .memberSupport(groupMemberId_): @@ -648,6 +651,7 @@ enum ChatResponse0: Decodable, ChatAPIResult { case chatStopped case apiChats(user: UserRef, chats: [ChatData]) case apiChat(user: UserRef, chat: ChatData, navInfo: NavigationInfo?) + case chatContentTypes(contentTypes: [MsgContentTag]) case chatTags(user: UserRef, userTags: [ChatTag]) case chatItemInfo(user: UserRef, chatItem: AChatItem, chatItemInfo: ChatItemInfo) case serverTestResult(user: UserRef, testServer: String, testFailure: ProtocolTestFailure?) @@ -680,6 +684,7 @@ enum ChatResponse0: Decodable, ChatAPIResult { case .chatStopped: "chatStopped" case .apiChats: "apiChats" case .apiChat: "apiChat" + case .chatContentTypes: "chatContentTypes" case .chatTags: "chatTags" case .chatItemInfo: "chatItemInfo" case .serverTestResult: "serverTestResult" @@ -714,6 +719,7 @@ enum ChatResponse0: Decodable, ChatAPIResult { case .chatStopped: return noDetails case let .apiChats(u, chats): return withUser(u, String(describing: chats)) case let .apiChat(u, chat, navInfo): return withUser(u, "chat: \(String(describing: chat))\nnavInfo: \(String(describing: navInfo))") + case let .chatContentTypes(types): return "content types: \(String(describing: types))" case let .chatTags(u, userTags): return withUser(u, "userTags: \(String(describing: userTags))") case let .chatItemInfo(u, chatItem, chatItemInfo): return withUser(u, "chatItem: \(String(describing: chatItem))\nchatItemInfo: \(String(describing: chatItemInfo))") case let .serverTestResult(u, server, testFailure): return withUser(u, "server: \(server)\nresult: \(String(describing: testFailure))") diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index 5a042a6252..52a0c343ff 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -444,6 +444,12 @@ func apiGetChat(chatId: ChatId, scope: GroupChatScope?, contentTag: MsgContentTa throw r.unexpected } +func apiGetChatContentTypes(chatId: ChatId, scope: GroupChatScope?) async throws -> [MsgContentTag] { + let r: ChatResponse0 = try await chatSendCmd(.apiGetChatContentTypes(chatId: chatId, scope: scope)) + if case let .chatContentTypes(types) = r { return types } + throw r.unexpected +} + func loadChat(chat: Chat, im: ItemsModel, search: String = "", clearItems: Bool = true) async { await loadChat(chatId: chat.chatInfo.id, im: im, search: search, clearItems: clearItems) } diff --git a/apps/ios/SimpleXChat/ChatTypes.swift b/apps/ios/SimpleXChat/ChatTypes.swift index 5d1d5b4302..e1bf8614e2 100644 --- a/apps/ios/SimpleXChat/ChatTypes.swift +++ b/apps/ios/SimpleXChat/ChatTypes.swift @@ -4601,7 +4601,7 @@ extension MsgContent: Encodable { } } -public enum MsgContentTag: String { +public enum MsgContentTag: String, Decodable { case text case link case image 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 a244293edb..b2817291ce 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 @@ -1036,6 +1036,14 @@ object ChatController { return null } + suspend fun apiGetChatContentTypes(rh: Long?, type: ChatType, id: Long, scope: GroupChatScope?): List? { + val r = sendCmd(rh, CC.ApiGetChatContentTypes(type, id, scope)) + if (r is API.Result && r.res is CR.ChatContentTypes) return r.res.contentTypes + Log.e(TAG, "apiGetChatContentTypes bad response: ${r.responseType} ${r.details}") + AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_loading_details), "${r.responseType}: ${r.details}") + return null + } + suspend fun apiCreateChatTag(rh: Long?, tag: ChatTagData): List? { val r = sendCmd(rh, CC.ApiCreateChatTag(tag)) if (r is API.Result && r.res is CR.ChatTags) return r.res.userTags @@ -3542,6 +3550,7 @@ sealed class CC { class ApiGetChatTags(val userId: Long): CC() class ApiGetChats(val userId: Long): CC() class ApiGetChat(val type: ChatType, val id: Long, val scope: GroupChatScope?, val contentTag: MsgContentTag?, val pagination: ChatPagination, val search: String = ""): CC() + class ApiGetChatContentTypes(val type: ChatType, val id: Long, val scope: GroupChatScope?): CC() class ApiGetChatItemInfo(val type: ChatType, val id: Long, val scope: GroupChatScope?, val itemId: Long): CC() class ApiSendMessages(val type: ChatType, val id: Long, val scope: GroupChatScope?, val live: Boolean, val ttl: Int?, val composedMessages: List): CC() class ApiCreateChatTag(val tag: ChatTagData): CC() @@ -3726,6 +3735,7 @@ sealed class CC { } "/_get chat ${chatRef(type, id, scope)}$tag ${pagination.cmdString}" + (if (search == "") "" else " search=$search") } + is ApiGetChatContentTypes -> "/_get content types ${chatRef(type, id, scope)})" is ApiGetChatItemInfo -> "/_get item info ${chatRef(type, id, scope)} $itemId" is ApiSendMessages -> { val msgs = json.encodeToString(composedMessages) @@ -3912,6 +3922,7 @@ sealed class CC { is ApiGetChatTags -> "apiGetChatTags" is ApiGetChats -> "apiGetChats" is ApiGetChat -> "apiGetChat" + is ApiGetChatContentTypes -> "apiGetChatContentTypes" is ApiGetChatItemInfo -> "apiGetChatItemInfo" is ApiSendMessages -> "apiSendMessages" is ApiCreateChatTag -> "apiCreateChatTag" @@ -6097,6 +6108,7 @@ sealed class CR { @Serializable @SerialName("chatStopped") class ChatStopped: CR() @Serializable @SerialName("apiChats") class ApiChats(val user: UserRef, val chats: List): CR() @Serializable @SerialName("apiChat") class ApiChat(val user: UserRef, val chat: Chat, val navInfo: NavigationInfo = NavigationInfo()): CR() + @Serializable @SerialName("chatContentTypes") class ChatContentTypes(val contentTypes: List): CR() @Serializable @SerialName("chatTags") class ChatTags(val user: UserRef, val userTags: List): CR() @Serializable @SerialName("chatItemInfo") class ApiChatItemInfo(val user: UserRef, val chatItem: AChatItem, val chatItemInfo: ChatItemInfo): CR() @Serializable @SerialName("serverTestResult") class ServerTestResult(val user: UserRef, val testServer: String, val testFailure: ProtocolTestFailure? = null): CR() @@ -6278,6 +6290,7 @@ sealed class CR { is ChatStopped -> "chatStopped" is ApiChats -> "apiChats" is ApiChat -> "apiChat" + is ChatContentTypes -> "chatContentTypes" is ChatTags -> "chatTags" is ApiChatItemInfo -> "chatItemInfo" is ServerTestResult -> "serverTestResult" @@ -6451,6 +6464,7 @@ sealed class CR { is ChatStopped -> noDetails() is ApiChats -> withUser(user, json.encodeToString(chats)) is ApiChat -> withUser(user, "remoteHostId: ${chat.remoteHostId}\nchatInfo: ${chat.chatInfo}\nchatStats: ${chat.chatStats}\nnavInfo: ${navInfo}\nchatItems: ${chat.chatItems}") + is ChatContentTypes -> "content types: ${json.encodeToString(contentTypes)}" is ChatTags -> withUser(user, "userTags: ${json.encodeToString(userTags)}") is ApiChatItemInfo -> withUser(user, "chatItem: ${json.encodeToString(chatItem)}\n${json.encodeToString(chatItemInfo)}") is ServerTestResult -> withUser(user, "server: $testServer\nresult: ${json.encodeToString(testFailure)}") From 0f65ba029125ab7c32a6b78ef941cb962f759fb5 Mon Sep 17 00:00:00 2001 From: Evgeny Date: Fri, 9 Jan 2026 12:33:41 +0000 Subject: [PATCH 09/73] website: directory under maintenance (#6557) --- website/src/directory.html | 1 + website/src/index.html | 1 + 2 files changed, 2 insertions(+) diff --git a/website/src/directory.html b/website/src/directory.html index 0235583ece..30d6765553 100644 --- a/website/src/directory.html +++ b/website/src/directory.html @@ -260,6 +260,7 @@ active_directory: true app.

SimpleX Directory is also available as a SimpleX chat bot.

Read about how to add your community.

+

Under maintenance — you can't join these groups until 17:00 UTC, 01/09.

From 4b986c4cf64445e3c616f5515e0f3491baa61f47 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Fri, 9 Jan 2026 18:17:28 +0000 Subject: [PATCH 10/73] Revert "website: directory under maintenance (#6557)" This reverts commit 0f65ba029125ab7c32a6b78ef941cb962f759fb5. --- website/src/directory.html | 1 - website/src/index.html | 1 - 2 files changed, 2 deletions(-) diff --git a/website/src/directory.html b/website/src/directory.html index 30d6765553..0235583ece 100644 --- a/website/src/directory.html +++ b/website/src/directory.html @@ -260,7 +260,6 @@ active_directory: true app.

SimpleX Directory is also available as a SimpleX chat bot.

Read about how to add your community.

-

Under maintenance — you can't join these groups until 17:00 UTC, 01/09.

From 8ebc8894de07a27816164f1ef8bb927b8a7f5f42 Mon Sep 17 00:00:00 2001 From: Evgeny Date: Sun, 11 Jan 2026 16:57:15 +0000 Subject: [PATCH 11/73] bots api: fix typescript code, add start/stop commands (#6565) --- bots/api/COMMANDS.md | 162 ++++++++++++------ bots/api/EVENTS.md | 2 +- bots/api/TYPES.md | 2 +- bots/src/API/Docs/Commands.hs | 20 ++- bots/src/API/Docs/Events.hs | 2 +- bots/src/API/Docs/Generate.hs | 2 +- bots/src/API/Docs/Generate/TypeScript.hs | 12 +- bots/src/API/Docs/Responses.hs | 8 +- bots/src/API/Docs/Syntax.hs | 6 +- .../types/typescript/package.json | 2 +- .../types/typescript/src/commands.ts | 53 ++++-- .../types/typescript/src/responses.ts | 18 ++ .../types/typescript/src/types.ts | 2 +- 13 files changed, 200 insertions(+), 91 deletions(-) diff --git a/bots/api/COMMANDS.md b/bots/api/COMMANDS.md index f3bb915665..b408d8eb30 100644 --- a/bots/api/COMMANDS.md +++ b/bots/api/COMMANDS.md @@ -60,6 +60,10 @@ This file is generated automatically. - [APIUpdateProfile](#apiupdateprofile) - [APISetContactPrefs](#apisetcontactprefs) +[Chat management](#chat-management) +- [StartChat](#startchat) +- [APIStopChat](#apistopchat) + --- @@ -98,7 +102,7 @@ UserContactLinkCreated: User contact address created. - user: [User](./TYPES.md#user) - connLinkContact: [CreatedConnLink](./TYPES.md#createdconnlink) -ChatCmdError: Command error. +ChatCmdError: Command error (only used in WebSockets API). - type: "chatCmdError" - chatError: [ChatError](./TYPES.md#chaterror) @@ -134,7 +138,7 @@ UserContactLinkDeleted: User contact address deleted. - type: "userContactLinkDeleted" - user: [User](./TYPES.md#user) -ChatCmdError: Command error. +ChatCmdError: Command error (only used in WebSockets API). - type: "chatCmdError" - chatError: [ChatError](./TYPES.md#chaterror) @@ -171,7 +175,7 @@ UserContactLink: User contact address. - user: [User](./TYPES.md#user) - contactLink: [UserContactLink](./TYPES.md#usercontactlink) -ChatCmdError: Command error. +ChatCmdError: Command error (only used in WebSockets API). - type: "chatCmdError" - chatError: [ChatError](./TYPES.md#chaterror) @@ -211,7 +215,7 @@ UserProfileUpdated: User profile updated. - toProfile: [Profile](./TYPES.md#profile) - updateSummary: [UserProfileUpdateSummary](./TYPES.md#userprofileupdatesummary) -ChatCmdError: Command error. +ChatCmdError: Command error (only used in WebSockets API). - type: "chatCmdError" - chatError: [ChatError](./TYPES.md#chaterror) @@ -249,7 +253,7 @@ UserContactLinkUpdated: User contact address updated. - user: [User](./TYPES.md#user) - contactLink: [UserContactLink](./TYPES.md#usercontactlink) -ChatCmdError: Command error. +ChatCmdError: Command error (only used in WebSockets API). - type: "chatCmdError" - chatError: [ChatError](./TYPES.md#chaterror) @@ -280,7 +284,7 @@ Send messages. ``` ```javascript -'/_send ' + sendRef.toString() + (liveMessage ? ' live=on' : '') + (ttl ? ' ttl=' + ttl : '') + ' json ' + JSON.stringify(composedMessages) // JavaScript +'/_send ' + ChatRef.cmdString(sendRef) + (liveMessage ? ' live=on' : '') + (ttl ? ' ttl=' + ttl : '') + ' json ' + JSON.stringify(composedMessages) // JavaScript ``` ```python @@ -294,7 +298,7 @@ NewChatItems: New messages. - user: [User](./TYPES.md#user) - chatItems: [[AChatItem](./TYPES.md#achatitem)] -ChatCmdError: Command error. +ChatCmdError: Command error (only used in WebSockets API). - type: "chatCmdError" - chatError: [ChatError](./TYPES.md#chaterror) @@ -320,7 +324,7 @@ Update message. ``` ```javascript -'/_update item ' + chatRef.toString() + ' ' + chatItemId + (liveMessage ? ' live=on' : '') + ' json ' + JSON.stringify(updatedMessage) // JavaScript +'/_update item ' + ChatRef.cmdString(chatRef) + ' ' + chatItemId + (liveMessage ? ' live=on' : '') + ' json ' + JSON.stringify(updatedMessage) // JavaScript ``` ```python @@ -339,7 +343,7 @@ ChatItemNotChanged: Message not changed. - user: [User](./TYPES.md#user) - chatItem: [AChatItem](./TYPES.md#achatitem) -ChatCmdError: Command error. +ChatCmdError: Command error (only used in WebSockets API). - type: "chatCmdError" - chatError: [ChatError](./TYPES.md#chaterror) @@ -367,7 +371,7 @@ Delete message. ``` ```javascript -'/_delete item ' + chatRef.toString() + ' ' + chatItemIds.join(',') + ' ' + deleteMode // JavaScript +'/_delete item ' + ChatRef.cmdString(chatRef) + ' ' + chatItemIds.join(',') + ' ' + deleteMode // JavaScript ``` ```python @@ -383,7 +387,7 @@ ChatItemsDeleted: Messages deleted. - byUser: bool - timed: bool -ChatCmdError: Command error. +ChatCmdError: Command error (only used in WebSockets API). - type: "chatCmdError" - chatError: [ChatError](./TYPES.md#chaterror) @@ -423,7 +427,7 @@ ChatItemsDeleted: Messages deleted. - byUser: bool - timed: bool -ChatCmdError: Command error. +ChatCmdError: Command error (only used in WebSockets API). - type: "chatCmdError" - chatError: [ChatError](./TYPES.md#chaterror) @@ -449,7 +453,7 @@ Add/remove message reaction. ``` ```javascript -'/_reaction ' + chatRef.toString() + ' ' + chatItemId + ' ' + (add ? 'on' : 'off') + ' ' + JSON.stringify(reaction) // JavaScript +'/_reaction ' + ChatRef.cmdString(chatRef) + ' ' + chatItemId + ' ' + (add ? 'on' : 'off') + ' ' + JSON.stringify(reaction) // JavaScript ``` ```python @@ -464,7 +468,7 @@ ChatItemReaction: Message reaction. - added: bool - reaction: [ACIReaction](./TYPES.md#acireaction) -ChatCmdError: Command error. +ChatCmdError: Command error (only used in WebSockets API). - type: "chatCmdError" - chatError: [ChatError](./TYPES.md#chaterror) @@ -515,7 +519,7 @@ RcvFileAcceptedSndCancelled: File accepted, but no longer sent. - user: [User](./TYPES.md#user) - rcvFileTransfer: [RcvFileTransfer](./TYPES.md#rcvfiletransfer) -ChatCmdError: Command error. +ChatCmdError: Command error (only used in WebSockets API). - type: "chatCmdError" - chatError: [ChatError](./TYPES.md#chaterror) @@ -560,7 +564,7 @@ RcvFileCancelled: Cancelled receiving file. - chatItem_: [AChatItem](./TYPES.md#achatitem)? - rcvFileTransfer: [RcvFileTransfer](./TYPES.md#rcvfiletransfer) -ChatCmdError: Command error. +ChatCmdError: Command error (only used in WebSockets API). - type: "chatCmdError" - chatError: [ChatError](./TYPES.md#chaterror) @@ -609,7 +613,7 @@ SentGroupInvitation: Group invitation sent. - contact: [Contact](./TYPES.md#contact) - member: [GroupMember](./TYPES.md#groupmember) -ChatCmdError: Command error. +ChatCmdError: Command error (only used in WebSockets API). - type: "chatCmdError" - chatError: [ChatError](./TYPES.md#chaterror) @@ -647,7 +651,7 @@ UserAcceptedGroupSent: User accepted group invitation. - groupInfo: [GroupInfo](./TYPES.md#groupinfo) - hostContact: [Contact](./TYPES.md#contact)? -ChatCmdError: Command error. +ChatCmdError: Command error (only used in WebSockets API). - type: "chatCmdError" - chatError: [ChatError](./TYPES.md#chaterror) @@ -687,7 +691,7 @@ MemberAccepted: Member accepted to group. - groupInfo: [GroupInfo](./TYPES.md#groupinfo) - member: [GroupMember](./TYPES.md#groupmember) -ChatCmdError: Command error. +ChatCmdError: Command error (only used in WebSockets API). - type: "chatCmdError" - chatError: [ChatError](./TYPES.md#chaterror) @@ -731,7 +735,7 @@ MembersRoleUser: Members role changed by user. - members: [[GroupMember](./TYPES.md#groupmember)] - toRole: [GroupMemberRole](./TYPES.md#groupmemberrole) -ChatCmdError: Command error. +ChatCmdError: Command error (only used in WebSockets API). - type: "chatCmdError" - chatError: [ChatError](./TYPES.md#chaterror) @@ -772,7 +776,7 @@ MembersBlockedForAllUser: Members blocked for all by admin. - members: [[GroupMember](./TYPES.md#groupmember)] - blocked: bool -ChatCmdError: Command error. +ChatCmdError: Command error (only used in WebSockets API). - type: "chatCmdError" - chatError: [ChatError](./TYPES.md#chaterror) @@ -813,7 +817,7 @@ UserDeletedMembers: Members deleted. - members: [[GroupMember](./TYPES.md#groupmember)] - withMessages: bool -ChatCmdError: Command error. +ChatCmdError: Command error (only used in WebSockets API). - type: "chatCmdError" - chatError: [ChatError](./TYPES.md#chaterror) @@ -853,7 +857,7 @@ LeftMemberUser: User left group. - user: [User](./TYPES.md#user) - groupInfo: [GroupInfo](./TYPES.md#groupinfo) -ChatCmdError: Command error. +ChatCmdError: Command error (only used in WebSockets API). - type: "chatCmdError" - chatError: [ChatError](./TYPES.md#chaterror) @@ -890,7 +894,7 @@ GroupMembers: Group members. - user: [User](./TYPES.md#user) - group: [Group](./TYPES.md#group) -ChatCmdError: Command error. +ChatCmdError: Command error (only used in WebSockets API). - type: "chatCmdError" - chatError: [ChatError](./TYPES.md#chaterror) @@ -929,7 +933,7 @@ GroupCreated: Group created. - user: [User](./TYPES.md#user) - groupInfo: [GroupInfo](./TYPES.md#groupinfo) -ChatCmdError: Command error. +ChatCmdError: Command error (only used in WebSockets API). - type: "chatCmdError" - chatError: [ChatError](./TYPES.md#chaterror) @@ -969,7 +973,7 @@ GroupUpdated: Group updated. - toGroup: [GroupInfo](./TYPES.md#groupinfo) - member_: [GroupMember](./TYPES.md#groupmember)? -ChatCmdError: Command error. +ChatCmdError: Command error (only used in WebSockets API). - type: "chatCmdError" - chatError: [ChatError](./TYPES.md#chaterror) @@ -1013,7 +1017,7 @@ GroupLinkCreated: Group link created. - groupInfo: [GroupInfo](./TYPES.md#groupinfo) - groupLink: [GroupLink](./TYPES.md#grouplink) -ChatCmdError: Command error. +ChatCmdError: Command error (only used in WebSockets API). - type: "chatCmdError" - chatError: [ChatError](./TYPES.md#chaterror) @@ -1052,7 +1056,7 @@ GroupLink: Group link. - groupInfo: [GroupInfo](./TYPES.md#groupinfo) - groupLink: [GroupLink](./TYPES.md#grouplink) -ChatCmdError: Command error. +ChatCmdError: Command error (only used in WebSockets API). - type: "chatCmdError" - chatError: [ChatError](./TYPES.md#chaterror) @@ -1089,7 +1093,7 @@ GroupLinkDeleted: Group link deleted. - user: [User](./TYPES.md#user) - groupInfo: [GroupInfo](./TYPES.md#groupinfo) -ChatCmdError: Command error. +ChatCmdError: Command error (only used in WebSockets API). - type: "chatCmdError" - chatError: [ChatError](./TYPES.md#chaterror) @@ -1127,7 +1131,7 @@ GroupLink: Group link. - groupInfo: [GroupInfo](./TYPES.md#groupinfo) - groupLink: [GroupLink](./TYPES.md#grouplink) -ChatCmdError: Command error. +ChatCmdError: Command error (only used in WebSockets API). - type: "chatCmdError" - chatError: [ChatError](./TYPES.md#chaterror) @@ -1171,7 +1175,7 @@ Invitation: One-time invitation. - connLinkInvitation: [CreatedConnLink](./TYPES.md#createdconnlink) - connection: [PendingContactConnection](./TYPES.md#pendingcontactconnection) -ChatCmdError: Command error. +ChatCmdError: Command error (only used in WebSockets API). - type: "chatCmdError" - chatError: [ChatError](./TYPES.md#chaterror) @@ -1210,7 +1214,7 @@ ConnectionPlan: Connection link information. - connLink: [CreatedConnLink](./TYPES.md#createdconnlink) - connectionPlan: [ConnectionPlan](./TYPES.md#connectionplan) -ChatCmdError: Command error. +ChatCmdError: Command error (only used in WebSockets API). - type: "chatCmdError" - chatError: [ChatError](./TYPES.md#chaterror) @@ -1219,7 +1223,7 @@ ChatCmdError: Command error. ### APIConnect -Connect via prepared SimpleX link. The link can be 1-time invitation link, contact address or group link +Connect via prepared SimpleX link. The link can be 1-time invitation link, contact address or group link. *Network usage*: interactive. @@ -1235,7 +1239,7 @@ Connect via prepared SimpleX link. The link can be 1-time invitation link, conta ``` ```javascript -'/_connect ' + userId + (preparedLink_ ? ' ' + preparedLink_.toString() : '') // JavaScript +'/_connect ' + userId + (preparedLink_ ? ' ' + CreatedConnLink.cmdString(preparedLink_) : '') // JavaScript ``` ```python @@ -1261,7 +1265,7 @@ SentInvitation: Invitation sent to contact address. - connection: [PendingContactConnection](./TYPES.md#pendingcontactconnection) - customUserProfile: [Profile](./TYPES.md#profile)? -ChatCmdError: Command error. +ChatCmdError: Command error (only used in WebSockets API). - type: "chatCmdError" - chatError: [ChatError](./TYPES.md#chaterror) @@ -1311,7 +1315,7 @@ SentInvitation: Invitation sent to contact address. - connection: [PendingContactConnection](./TYPES.md#pendingcontactconnection) - customUserProfile: [Profile](./TYPES.md#profile)? -ChatCmdError: Command error. +ChatCmdError: Command error (only used in WebSockets API). - type: "chatCmdError" - chatError: [ChatError](./TYPES.md#chaterror) @@ -1348,7 +1352,7 @@ AcceptingContactRequest: Contact request accepted. - user: [User](./TYPES.md#user) - contact: [Contact](./TYPES.md#contact) -ChatCmdError: Command error. +ChatCmdError: Command error (only used in WebSockets API). - type: "chatCmdError" - chatError: [ChatError](./TYPES.md#chaterror) @@ -1386,7 +1390,7 @@ ContactRequestRejected: Contact request rejected. - contactRequest: [UserContactRequest](./TYPES.md#usercontactrequest) - contact_: [Contact](./TYPES.md#contact)? -ChatCmdError: Command error. +ChatCmdError: Command error (only used in WebSockets API). - type: "chatCmdError" - chatError: [ChatError](./TYPES.md#chaterror) @@ -1428,7 +1432,7 @@ ContactsList: Contacts. - user: [User](./TYPES.md#user) - contacts: [[Contact](./TYPES.md#contact)] -ChatCmdError: Command error. +ChatCmdError: Command error (only used in WebSockets API). - type: "chatCmdError" - chatError: [ChatError](./TYPES.md#chaterror) @@ -1467,7 +1471,7 @@ GroupsList: Groups. - user: [User](./TYPES.md#user) - groups: [[GroupInfo](./TYPES.md#groupinfo)] -ChatCmdError: Command error. +ChatCmdError: Command error (only used in WebSockets API). - type: "chatCmdError" - chatError: [ChatError](./TYPES.md#chaterror) @@ -1491,7 +1495,7 @@ Delete chat. ``` ```javascript -'/_delete ' + chatRef.toString() + ' ' + chatDeleteMode.toString() // JavaScript +'/_delete ' + ChatRef.cmdString(chatRef) + ' ' + ChatDeleteMode.cmdString(chatDeleteMode) // JavaScript ``` ```python @@ -1515,7 +1519,7 @@ GroupDeletedUser: User deleted group. - user: [User](./TYPES.md#user) - groupInfo: [GroupInfo](./TYPES.md#groupinfo) -ChatCmdError: Command error. +ChatCmdError: Command error (only used in WebSockets API). - type: "chatCmdError" - chatError: [ChatError](./TYPES.md#chaterror) @@ -1529,7 +1533,7 @@ Most bots don't need to use these commands, as bot profile can be configured man ### ShowActiveUser -Get active user profile +Get active user profile. *Network usage*: no. @@ -1545,7 +1549,7 @@ ActiveUser: Active user profile. - type: "activeUser" - user: [User](./TYPES.md#user) -ChatCmdError: Command error. +ChatCmdError: Command error (only used in WebSockets API). - type: "chatCmdError" - chatError: [ChatError](./TYPES.md#chaterror) @@ -1554,7 +1558,7 @@ ChatCmdError: Command error. ### CreateActiveUser -Create new user profile +Create new user profile. *Network usage*: no. @@ -1581,7 +1585,7 @@ ActiveUser: Active user profile. - type: "activeUser" - user: [User](./TYPES.md#user) -ChatCmdError: Command error. +ChatCmdError: Command error (only used in WebSockets API). - type: "chatCmdError" - chatError: [ChatError](./TYPES.md#chaterror) @@ -1594,7 +1598,7 @@ ChatCmdError: Command error. ### ListUsers -Get all user profiles +Get all user profiles. *Network usage*: no. @@ -1610,7 +1614,7 @@ UsersList: Users. - type: "usersList" - users: [[UserInfo](./TYPES.md#userinfo)] -ChatCmdError: Command error. +ChatCmdError: Command error (only used in WebSockets API). - type: "chatCmdError" - chatError: [ChatError](./TYPES.md#chaterror) @@ -1619,7 +1623,7 @@ ChatCmdError: Command error. ### APISetActiveUser -Set active user profile +Set active user profile. *Network usage*: no. @@ -1647,7 +1651,7 @@ ActiveUser: Active user profile. - type: "activeUser" - user: [User](./TYPES.md#user) -ChatCmdError: Command error. +ChatCmdError: Command error (only used in WebSockets API). - type: "chatCmdError" - chatError: [ChatError](./TYPES.md#chaterror) @@ -1688,7 +1692,7 @@ CmdOk: Ok. - type: "cmdOk" - user_: [User](./TYPES.md#user)? -ChatCmdError: Command error. +ChatCmdError: Command error (only used in WebSockets API). - type: "chatCmdError" - chatError: [ChatError](./TYPES.md#chaterror) @@ -1732,7 +1736,7 @@ UserProfileNoChange: User profile was not changed. - type: "userProfileNoChange" - user: [User](./TYPES.md#user) -ChatCmdError: Command error. +ChatCmdError: Command error (only used in WebSockets API). - type: "chatCmdError" - chatError: [ChatError](./TYPES.md#chaterror) @@ -1771,8 +1775,60 @@ ContactPrefsUpdated: Contact preferences updated. - fromContact: [Contact](./TYPES.md#contact) - toContact: [Contact](./TYPES.md#contact) -ChatCmdError: Command error. +ChatCmdError: Command error (only used in WebSockets API). - type: "chatCmdError" - chatError: [ChatError](./TYPES.md#chaterror) --- + + +## Chat management + +These commands should not be used with CLI-based bots + + +### StartChat + +Start chat controller. + +*Network usage*: no. + +**Parameters**: +- mainApp: bool +- enableSndFiles: bool + +**Syntax**: + +``` +/_start +``` + +**Responses**: + +ChatStarted: Chat started. +- type: "chatStarted" + +ChatRunning: Chat running. +- type: "chatRunning" + +--- + + +### APIStopChat + +Stop chat controller. + +*Network usage*: no. + +**Syntax**: + +``` +/_stop +``` + +**Response**: + +ChatStopped: Chat stopped. +- type: "chatStopped" + +--- diff --git a/bots/api/EVENTS.md b/bots/api/EVENTS.md index a77747482f..84f59e7405 100644 --- a/bots/api/EVENTS.md +++ b/bots/api/EVENTS.md @@ -705,7 +705,7 @@ Message error. ### ChatError -Chat error. +Chat error (only used in WebSockets API). **Record type**: - type: "chatError" diff --git a/bots/api/TYPES.md b/bots/api/TYPES.md index 62c5dff3b5..1aaa291204 100644 --- a/bots/api/TYPES.md +++ b/bots/api/TYPES.md @@ -1309,7 +1309,7 @@ Used in API commands. Chat scope can only be passed with groups. ``` ```javascript -chatType.toString() + chatId + (chatScope ? chatScope.toString() : '') // JavaScript +ChatType.cmdString(chatType) + chatId + (chatScope ? GroupChatScope.cmdString(chatScope) : '') // JavaScript ``` ```python diff --git a/bots/src/API/Docs/Commands.hs b/bots/src/API/Docs/Commands.hs index 41e8cdf019..35745f9b42 100644 --- a/bots/src/API/Docs/Commands.hs +++ b/bots/src/API/Docs/Commands.hs @@ -72,7 +72,7 @@ instance IsString ErrorTypeDoc where fromString s = TD s "" -- category name, category description, commands --- inner: constructor, description, responses, errors (ChatErrorType constructors), network usage, syntax +-- inner: constructor, hidden params, description, responses, errors (ChatErrorType constructors), network usage, syntax chatCommandsDocsData :: [(String, String, [(ConsName, [String], Text, [ConsName], [ErrorTypeDoc], Maybe UsesNetwork, Expr)])] chatCommandsDocsData = [ ( "Address commands", @@ -132,7 +132,7 @@ chatCommandsDocsData = "These commands may be used to create connections. Most bots do not need to use them - bot users will connect via bot address with auto-accept enabled.", [ ("APIAddContact", [], "Create 1-time invitation link.", ["CRInvitation", "CRChatCmdError"], [], Just UNInteractive, "/_connect " <> Param "userId" <> OnOffParam "incognito" "incognito" (Just False)), ("APIConnectPlan", [], "Determine SimpleX link type and if the bot is already connected via this link.", ["CRConnectionPlan", "CRChatCmdError"], [], Just UNInteractive, "/_connect plan " <> Param "userId" <> " " <> Param "connectionLink"), - ("APIConnect", [], "Connect via prepared SimpleX link. The link can be 1-time invitation link, contact address or group link", ["CRSentConfirmation", "CRContactAlreadyExists", "CRSentInvitation", "CRChatCmdError"], [], Just UNInteractive, "/_connect " <> Param "userId" <> Optional "" (" " <> Param "$0") "preparedLink_"), + ("APIConnect", [], "Connect via prepared SimpleX link. The link can be 1-time invitation link, contact address or group link.", ["CRSentConfirmation", "CRContactAlreadyExists", "CRSentInvitation", "CRChatCmdError"], [], Just UNInteractive, "/_connect " <> Param "userId" <> Optional "" (" " <> Param "$0") "preparedLink_"), ("Connect", [], "Connect via SimpleX link as string in the active user profile.", ["CRSentConfirmation", "CRContactAlreadyExists", "CRSentInvitation", "CRChatCmdError"], [], Just UNInteractive, "/connect" <> Optional "" (" " <> Param "$0") "connLink_"), ("APIAcceptContact", ["incognito"], "Accept contact request.", ["CRAcceptingContactRequest", "CRChatCmdError"], [], Just UNInteractive, "/_accept " <> Param "contactReqId"), ("APIRejectContact", [], "Reject contact request. The user who sent the request is **not notified**.", ["CRContactRequestRejected", "CRChatCmdError"], [], Nothing, "/_reject " <> Param "contactReqId") @@ -163,21 +163,27 @@ chatCommandsDocsData = ), ( "User profile commands", "Most bots don't need to use these commands, as bot profile can be configured manually via CLI or desktop client. These commands can be used by bots that need to manage multiple user profiles (e.g., the profiles of support agents).", - [ ("ShowActiveUser", [], "Get active user profile", ["CRActiveUser", "CRChatCmdError"], [], Nothing, "/user"), + [ ("ShowActiveUser", [], "Get active user profile.", ["CRActiveUser", "CRChatCmdError"], [], Nothing, "/user"), ( "CreateActiveUser", [], - "Create new user profile", + "Create new user profile.", ["CRActiveUser", "CRChatCmdError"], [TD "CEUserExists" "User or contact with this name already exists", TD "CEInvalidDisplayName" "Invalid user display name"], Nothing, "/_create user " <> Json "newUser" ), - ("ListUsers", [], "Get all user profiles", ["CRUsersList", "CRChatCmdError"], [], Nothing, "/users"), - ("APISetActiveUser", [], "Set active user profile", ["CRActiveUser", "CRChatCmdError"], ["CEChatNotStarted"], Nothing, "/_user " <> Param "userId" <> Optional "" (" " <> Json "$0") "viewPwd"), + ("ListUsers", [], "Get all user profiles.", ["CRUsersList", "CRChatCmdError"], [], Nothing, "/users"), + ("APISetActiveUser", [], "Set active user profile.", ["CRActiveUser", "CRChatCmdError"], ["CEChatNotStarted"], Nothing, "/_user " <> Param "userId" <> Optional "" (" " <> Json "$0") "viewPwd"), ("APIDeleteUser", [], "Delete user profile.", ["CRCmdOk", "CRChatCmdError"], [], Just UNBackground, "/_delete user " <> Param "userId" <> OnOffParam "del_smp" "delSMPQueues" Nothing <> Optional "" (" " <> Json "$0") "viewPwd"), ("APIUpdateProfile", [], "Update user profile.", ["CRUserProfileUpdated", "CRUserProfileNoChange", "CRChatCmdError"], [], Just UNBackground, "/_profile " <> Param "userId" <> " " <> Json "profile"), ("APISetContactPrefs", [], "Configure chat preference overrides for the contact.", ["CRContactPrefsUpdated", "CRChatCmdError"], [], Just UNBackground, "/_set prefs @" <> Param "contactId" <> " " <> Json "preferences") ] + ), + ( "Chat management", + "These commands should not be used with CLI-based bots", + [ ("StartChat", [], "Start chat controller.", ["CRChatStarted", "CRChatRunning"], [], Nothing, "/_start"), + ("APIStopChat", [], "Stop chat controller.", ["CRChatStopped"], [], Nothing, "/_stop") + ] ) ] @@ -396,7 +402,6 @@ undocumentedCommands = "APISetUserServers", "APISetUserUIThemes", "APIStandaloneFileInfo", - "APIStopChat", "APIStorageEncryption", "APISuspendChat", "APISwitchContact", @@ -453,7 +458,6 @@ undocumentedCommands = "SetTempFolder", "SetUserProtoServers", "SlowSQLQueries", - "StartChat", "StartRemoteHost", "StopRemoteCtrl", "StopRemoteHost", diff --git a/bots/src/API/Docs/Events.hs b/bots/src/API/Docs/Events.hs index 4a49551a54..ecb8a5ba04 100644 --- a/bots/src/API/Docs/Events.hs +++ b/bots/src/API/Docs/Events.hs @@ -143,7 +143,7 @@ chatEventsDocsData = \or because messages may be delivered to deleted chats for a short period of time \ \(they will be ignored).", [ ("CEvtMessageError", ""), - ("CEvtChatError", ""), -- only used in WebSockets API, Haskell code uses Either, with error in Left + ("CEvtChatError", "Chat error (only used in WebSockets API)."), -- Haskell code uses Either, with error in Left ("CEvtChatErrors", "") ], [] diff --git a/bots/src/API/Docs/Generate.hs b/bots/src/API/Docs/Generate.hs index 334ad93bad..99886bf222 100644 --- a/bots/src/API/Docs/Generate.hs +++ b/bots/src/API/Docs/Generate.hs @@ -73,7 +73,7 @@ syntaxText :: TypeAndFields -> Expr -> Text syntaxText r syntax = "\n**Syntax**:\n" <> "\n```\n" <> docSyntaxText r syntax <> "\n```\n" - <> (if isConst syntax then "" else "\n```javascript\n" <> jsSyntaxText False r syntax <> " // JavaScript\n```\n") + <> (if isConst syntax then "" else "\n```javascript\n" <> jsSyntaxText False "" r syntax <> " // JavaScript\n```\n") <> (if isConst syntax then "" else "\n```python\n" <> pySyntaxText r syntax <> " # Python\n```\n") camelToSpace :: String -> String diff --git a/bots/src/API/Docs/Generate/TypeScript.hs b/bots/src/API/Docs/Generate/TypeScript.hs index b69635086e..c3049c100d 100644 --- a/bots/src/API/Docs/Generate/TypeScript.hs +++ b/bots/src/API/Docs/Generate/TypeScript.hs @@ -49,7 +49,7 @@ commandsCodeText = <> "}\n\n" <> ("export namespace " <> T.pack constrName <> " {\n") <> (" export type Response = " <> constrsCode " " "CR" (("CR." <> ) . T.pack . fstToUpper . memberTag) (map responseType responses)) - <> (if syntax == "" then "" else funcCode APITypeDef {typeName' = constrName, typeDef = ATDRecord params} syntax) + <> (if syntax == "" then "" else funcCode APITypeDef {typeName' = constrName, typeDef = ATDRecord params} "T." syntax) <> "}\n" where constrName = fstToUpper tag @@ -86,7 +86,7 @@ typesCodeText = ("// API Types\n// " <> autoGenerated <> "\n") <> foldMap typeCo "ConnectionMode" -> T.pack $ map toUpper tag "FileProtocol" -> T.pack $ map toUpper tag _ -> T.replace "-" "_" $ T.pack $ fstToUpper tag - namespaceFuncCode = "\nexport namespace " <> name' <> " {" <> funcCode td typeSyntax <> "}\n" + namespaceFuncCode = "\nexport namespace " <> name' <> " {" <> funcCode td "" typeSyntax <> "}\n" typeDefCode = case typeDef of ATDRecord fields -> ("\nexport interface " <> name' <> " {\n") @@ -107,7 +107,7 @@ unionTypeCode unionNamespace typesNamespace td@APITypeDef {typeName' = name} cs <> (" export type Tag = " <> constrsCode " " name' constrTag (L.toList cs) <> "\n") <> (" interface Interface {\n type: Tag\n }\n") <> foldMap constrType cs - <> (if cmdSyntax == "" then "" else funcCode td cmdSyntax) + <> (if cmdSyntax == "" then "" else funcCode td typesNamespace cmdSyntax) <> "}\n" where name' = T.pack name @@ -128,9 +128,9 @@ constrsCode indent name' constr cs line = T.intercalate " | " cs' cs' = map constr cs -funcCode :: APITypeDef -> Expr -> Text -funcCode td@APITypeDef {typeName' = name, typeDef} cmdSyntax = - "\n export function cmdString(" <> param <> ": " <> T.pack name <> "): string {\n return " <> jsSyntaxText True (name, self : typeFields) cmdSyntax <> "\n }\n" +funcCode :: APITypeDef -> String -> Expr -> Text +funcCode td@APITypeDef {typeName' = name, typeDef} typeNamespace cmdSyntax = + "\n export function cmdString(" <> param <> ": " <> T.pack name <> "): string {\n return " <> jsSyntaxText True typeNamespace (name, self : typeFields) cmdSyntax <> "\n }\n" where param = if hasParams cmdSyntax then "self" else "_self" self = APIRecordField "self" (ATDef td) diff --git a/bots/src/API/Docs/Responses.hs b/bots/src/API/Docs/Responses.hs index c52b288603..60fe129cdb 100644 --- a/bots/src/API/Docs/Responses.hs +++ b/bots/src/API/Docs/Responses.hs @@ -51,8 +51,11 @@ chatResponsesDocsData = ("CRChatItemReaction", "Message reaction"), ("CRChatItemUpdated", "Message updated"), ("CRChatItemsDeleted", "Messages deleted"), + ("CRChatRunning", ""), + ("CRChatStarted", ""), + ("CRChatStopped", ""), ("CRCmdOk", "Ok"), - ("CRChatCmdError", "Command error"), -- only used in WebSockets API, Haskell code uses Either, with error in Left + ("CRChatCmdError", "Command error (only used in WebSockets API)"), -- Haskell code uses Either, with error in Left ("CRConnectionPlan", "Connection link information"), ("CRContactAlreadyExists", ""), ("CRContactConnectionDeleted", "Connection deleted"), @@ -127,10 +130,7 @@ undocumentedResponses = "CRChatItemInfo", "CRChatItems", "CRChatItemTTL", - "CRChatRunning", "CRChats", - "CRChatStarted", - "CRChatStopped", "CRConnectionsDiff", "CRChatTags", "CRConnectionAliasUpdated", diff --git a/bots/src/API/Docs/Syntax.hs b/bots/src/API/Docs/Syntax.hs index 83fa2bf6a2..f96ec03b02 100644 --- a/bots/src/API/Docs/Syntax.hs +++ b/bots/src/API/Docs/Syntax.hs @@ -99,8 +99,8 @@ withOptBoolParam r param p f = (ATOptional (ATPrim (PT TBool))) -> f True _ -> paramError r param p "is not [optional] boolean" -jsSyntaxText :: Bool -> TypeAndFields -> Expr -> Text -jsSyntaxText useSelf r = T.replace "' + '" "" . T.pack . go Nothing True +jsSyntaxText :: Bool -> String -> TypeAndFields -> Expr -> Text +jsSyntaxText useSelf typeNamespace r = T.replace "' + '" "" . T.pack . go Nothing True where go param top = \case Concat exs -> intercalate " + " $ map (go param False) $ L.toList exs @@ -112,7 +112,7 @@ jsSyntaxText useSelf r = T.replace "' + '" "" . T.pack . go Nothing True _ -> paramName' useSelf param p where toStringSyntax (APITypeDef typeName _) - | typeHasSyntax typeName = paramName' useSelf param p <> ".toString()" + | typeHasSyntax typeName = typeNamespace <> typeName <> ".cmdString(" <> paramName' useSelf param p <> ")" | otherwise = paramName' useSelf param p Optional exN exJ p -> open <> n <> " ? " <> go (Just p) False exJ <> " : " <> nothing <> close where diff --git a/packages/simplex-chat-client/types/typescript/package.json b/packages/simplex-chat-client/types/typescript/package.json index 1bf593b483..c24b93fbed 100644 --- a/packages/simplex-chat-client/types/typescript/package.json +++ b/packages/simplex-chat-client/types/typescript/package.json @@ -1,6 +1,6 @@ { "name": "@simplex-chat/types", - "version": "0.1.0", + "version": "0.2.0", "description": "TypeScript types for SimpleX Chat bot libraries", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/simplex-chat-client/types/typescript/src/commands.ts b/packages/simplex-chat-client/types/typescript/src/commands.ts index de6a7a7ce1..66f4f6ec5f 100644 --- a/packages/simplex-chat-client/types/typescript/src/commands.ts +++ b/packages/simplex-chat-client/types/typescript/src/commands.ts @@ -96,7 +96,7 @@ export namespace APISendMessages { export type Response = CR.NewChatItems | CR.ChatCmdError export function cmdString(self: APISendMessages): string { - return '/_send ' + self.sendRef.toString() + (self.liveMessage ? ' live=on' : '') + (self.ttl ? ' ttl=' + self.ttl : '') + ' json ' + JSON.stringify(self.composedMessages) + return '/_send ' + T.ChatRef.cmdString(self.sendRef) + (self.liveMessage ? ' live=on' : '') + (self.ttl ? ' ttl=' + self.ttl : '') + ' json ' + JSON.stringify(self.composedMessages) } } @@ -113,7 +113,7 @@ export namespace APIUpdateChatItem { export type Response = CR.ChatItemUpdated | CR.ChatItemNotChanged | CR.ChatCmdError export function cmdString(self: APIUpdateChatItem): string { - return '/_update item ' + self.chatRef.toString() + ' ' + self.chatItemId + (self.liveMessage ? ' live=on' : '') + ' json ' + JSON.stringify(self.updatedMessage) + return '/_update item ' + T.ChatRef.cmdString(self.chatRef) + ' ' + self.chatItemId + (self.liveMessage ? ' live=on' : '') + ' json ' + JSON.stringify(self.updatedMessage) } } @@ -129,7 +129,7 @@ export namespace APIDeleteChatItem { export type Response = CR.ChatItemsDeleted | CR.ChatCmdError export function cmdString(self: APIDeleteChatItem): string { - return '/_delete item ' + self.chatRef.toString() + ' ' + self.chatItemIds.join(',') + ' ' + self.deleteMode + return '/_delete item ' + T.ChatRef.cmdString(self.chatRef) + ' ' + self.chatItemIds.join(',') + ' ' + self.deleteMode } } @@ -161,7 +161,7 @@ export namespace APIChatItemReaction { export type Response = CR.ChatItemReaction | CR.ChatCmdError export function cmdString(self: APIChatItemReaction): string { - return '/_reaction ' + self.chatRef.toString() + ' ' + self.chatItemId + ' ' + (self.add ? 'on' : 'off') + ' ' + JSON.stringify(self.reaction) + return '/_reaction ' + T.ChatRef.cmdString(self.chatRef) + ' ' + self.chatItemId + ' ' + (self.add ? 'on' : 'off') + ' ' + JSON.stringify(self.reaction) } } @@ -450,7 +450,7 @@ export namespace APIConnectPlan { } } -// Connect via prepared SimpleX link. The link can be 1-time invitation link, contact address or group link +// Connect via prepared SimpleX link. The link can be 1-time invitation link, contact address or group link. // Network usage: interactive. export interface APIConnect { userId: number // int64 @@ -462,7 +462,7 @@ export namespace APIConnect { export type Response = CR.SentConfirmation | CR.ContactAlreadyExists | CR.SentInvitation | CR.ChatCmdError export function cmdString(self: APIConnect): string { - return '/_connect ' + self.userId + (self.preparedLink_ ? ' ' + self.preparedLink_.toString() : '') + return '/_connect ' + self.userId + (self.preparedLink_ ? ' ' + T.CreatedConnLink.cmdString(self.preparedLink_) : '') } } @@ -553,14 +553,14 @@ export namespace APIDeleteChat { export type Response = CR.ContactDeleted | CR.ContactConnectionDeleted | CR.GroupDeletedUser | CR.ChatCmdError export function cmdString(self: APIDeleteChat): string { - return '/_delete ' + self.chatRef.toString() + ' ' + self.chatDeleteMode.toString() + return '/_delete ' + T.ChatRef.cmdString(self.chatRef) + ' ' + T.ChatDeleteMode.cmdString(self.chatDeleteMode) } } // User profile commands // Most bots don't need to use these commands, as bot profile can be configured manually via CLI or desktop client. These commands can be used by bots that need to manage multiple user profiles (e.g., the profiles of support agents). -// Get active user profile +// Get active user profile. // Network usage: no. export interface ShowActiveUser { } @@ -573,7 +573,7 @@ export namespace ShowActiveUser { } } -// Create new user profile +// Create new user profile. // Network usage: no. export interface CreateActiveUser { newUser: T.NewUser @@ -587,7 +587,7 @@ export namespace CreateActiveUser { } } -// Get all user profiles +// Get all user profiles. // Network usage: no. export interface ListUsers { } @@ -600,7 +600,7 @@ export namespace ListUsers { } } -// Set active user profile +// Set active user profile. // Network usage: no. export interface APISetActiveUser { userId: number // int64 @@ -660,3 +660,34 @@ export namespace APISetContactPrefs { return '/_set prefs @' + self.contactId + ' ' + JSON.stringify(self.preferences) } } + +// Chat management +// These commands should not be used with CLI-based bots + +// Start chat controller. +// Network usage: no. +export interface StartChat { + mainApp: boolean + enableSndFiles: boolean +} + +export namespace StartChat { + export type Response = CR.ChatStarted | CR.ChatRunning + + export function cmdString(_self: StartChat): string { + return '/_start' + } +} + +// Stop chat controller. +// Network usage: no. +export interface APIStopChat { +} + +export namespace APIStopChat { + export type Response = CR.ChatStopped + + export function cmdString(_self: APIStopChat): string { + return '/_stop' + } +} diff --git a/packages/simplex-chat-client/types/typescript/src/responses.ts b/packages/simplex-chat-client/types/typescript/src/responses.ts index ea49478b15..684aeec7af 100644 --- a/packages/simplex-chat-client/types/typescript/src/responses.ts +++ b/packages/simplex-chat-client/types/typescript/src/responses.ts @@ -10,6 +10,9 @@ export type ChatResponse = | CR.ChatItemReaction | CR.ChatItemUpdated | CR.ChatItemsDeleted + | CR.ChatRunning + | CR.ChatStarted + | CR.ChatStopped | CR.CmdOk | CR.ChatCmdError | CR.ConnectionPlan @@ -58,6 +61,9 @@ export namespace CR { | "chatItemReaction" | "chatItemUpdated" | "chatItemsDeleted" + | "chatRunning" + | "chatStarted" + | "chatStopped" | "cmdOk" | "chatCmdError" | "connectionPlan" @@ -140,6 +146,18 @@ export namespace CR { timed: boolean } + export interface ChatRunning extends Interface { + type: "chatRunning" + } + + export interface ChatStarted extends Interface { + type: "chatStarted" + } + + export interface ChatStopped extends Interface { + type: "chatStopped" + } + export interface CmdOk extends Interface { type: "cmdOk" user_?: T.User diff --git a/packages/simplex-chat-client/types/typescript/src/types.ts b/packages/simplex-chat-client/types/typescript/src/types.ts index 5e3309238b..bca0179a91 100644 --- a/packages/simplex-chat-client/types/typescript/src/types.ts +++ b/packages/simplex-chat-client/types/typescript/src/types.ts @@ -1552,7 +1552,7 @@ export interface ChatRef { export namespace ChatRef { export function cmdString(self: ChatRef): string { - return self.chatType.toString() + self.chatId + (self.chatScope ? self.chatScope.toString() : '') + return ChatType.cmdString(self.chatType) + self.chatId + (self.chatScope ? GroupChatScope.cmdString(self.chatScope) : '') } } From bf1783feb46065332d83bef452751b28a7d76751 Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Mon, 12 Jan 2026 10:16:32 +0000 Subject: [PATCH 12/73] core: fix agent cleanup manager not starting in normal operation mode in CLI (#6567) --- src/Simplex/Chat/Core.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Simplex/Chat/Core.hs b/src/Simplex/Chat/Core.hs index 131b420bd9..5837e64f4d 100644 --- a/src/Simplex/Chat/Core.hs +++ b/src/Simplex/Chat/Core.hs @@ -52,7 +52,7 @@ simplexChatCore cfg@ChatConfig {confirmMigrations, testView, chatHooks} opts@Cha exitFailure run db@ChatDatabase {chatStore} = do u_ <- getSelectActiveUser chatStore - let backgroundMode = not maintenance + let backgroundMode = maintenance cc <- newChatController db u_ cfg opts backgroundMode u <- maybe (createActiveUser cc createBot) pure u_ unless testView $ putStrLn $ "Current user: " <> userStr u From 324aefef862477acc003686a0ce74990f54db459 Mon Sep 17 00:00:00 2001 From: sh <37271604+shumvgolove@users.noreply.github.com> Date: Mon, 12 Jan 2026 17:30:35 +0000 Subject: [PATCH 13/73] ci: add nodejs action (#6568) * ci: add nodejs action * ci: better curl flags * ci: fix naming --- .github/workflows/build.yml | 71 +++++++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f73bfa7927..05bf62f7fa 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -582,3 +582,74 @@ jobs: bin_hash: ${{ steps.windows_desktop_build.outputs.package_hash }} github_ref: ${{ github.ref }} github_token: ${{ secrets.GITHUB_TOKEN }} + +# ========================= +# NodeJS libs release +# ========================= + +# Downloads Desktop builds, extracts and archives libraries for NodeJS addon. +# Depends on Linux/MacOS, executes only on release. + +# Secrets: +# ------- +# NODEJS_REPO_TOKEN +# Only select repositories: simplex-chat-libs +# Permissions: +# * Contents (Read and Write) + + release-nodejs-libs: + runs-on: ubuntu-latest + needs: [build-linux, build-macos] + if: startsWith(github.ref, 'refs/tags/v') && (!cancelled()) + steps: + - name: Checkout current repository + uses: actions/checkout@v6 + + - name: Build archives + run: | + INIT_DIR='${{ runner.temp }}/artifacts' + RELEASE_DIR='${{ runner.temp }}/release-assets' + TAG='${{ github.ref_name }}' + URL='https://github.com/${{ github.repository }}/releases/download' + PREFIX='${{ github.event.repository.name }}-libs' + + # Setup directories + mkdir "$INIT_DIR" "$RELEASE_DIR" && cd "$INIT_DIR" + + # Downlaod desktop release + curl --proto '=https' --tlsv1.2 -sSf -L "${URL}/${TAG}/simplex-desktop-ubuntu-22_04-x86_64.deb" -o linux.deb + curl --proto '=https' --tlsv1.2 -sSf -L "${URL}/${TAG}/simplex-desktop-macos-aarch64.dmg" -o macos-aarch64.dmg + curl --proto '=https' --tlsv1.2 -sSf -L "${URL}/${TAG}/simplex-desktop-macos-x86_64.dmg" -o macos-x86_64.dmg + + # Linux + # ----- + # Extract libraries + dpkg-deb -R linux.deb linux-out/ && cd linux-out/opt/simplex/lib/app/resources + # Preprare directory + mkdir libs && cp *.so libs/ + # Archive + zip -r "${PREFIX}-linux-x86_64.zip" libs + # Back to original dir + mv "${PREFIX}-linux-x86_64.zip" "$RELEASE_DIR" && cd "$INIT_DIR" + + # MacOS: aarch64 + # -------------- + 7z x macos-aarch64.dmg -omacos1-out/ && cd macos1-out/SimpleX/SimpleX.app/Contents/app/resources/ + mkdir libs && cp *.dylib libs/ + zip -r "${PREFIX}-macos-aarch64.zip" libs + mv "${PREFIX}-macos-aarch64.zip" "$RELEASE_DIR" && cd "$INIT_DIR" + + # Macos: x86_64 + # ------------- + 7z x macos-x86_64.dmg -omacos2-out/ && cd macos2-out/SimpleX/SimpleX.app/Contents/app/resources/ + mkdir libs && cp *.dylib libs/ + zip -r "${PREFIX}-macos-x86_64.zip" libs + mv "${PREFIX}-macos-x86_64.zip" "$RELEASE_DIR" && cd "$INIT_DIR" + + - name: Create release in libs repo and upload artifacts + uses: softprops/action-gh-release@v2 + with: + repository: ${{ github.repository }}-libs + tag_name: ${{ github.ref_name }} + files: ${{ runner.temp }}/release-assets/* + token: ${{ secrets.NODEJS_REPO_TOKEN }} From 8e5481611b77971f08f8434b3e9c7cc61d617e3b Mon Sep 17 00:00:00 2001 From: Evgeny Date: Tue, 13 Jan 2026 12:05:47 +0000 Subject: [PATCH 14/73] website: directory under maintenance (#6571) * website: directory under maintenance (#6557) * update --- website/src/directory.html | 1 + website/src/index.html | 1 + 2 files changed, 2 insertions(+) diff --git a/website/src/directory.html b/website/src/directory.html index 0235583ece..0e2390b591 100644 --- a/website/src/directory.html +++ b/website/src/directory.html @@ -260,6 +260,7 @@ active_directory: true app.

SimpleX Directory is also available as a SimpleX chat bot.

Read about how to add your community.

+

Under maintenance — you can't join these groups until 14:00 UTC, 01/13.

From 6f8f684c6fb737c3be3c892eec5a387f34718f72 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Tue, 13 Jan 2026 15:02:33 +0000 Subject: [PATCH 15/73] Revert "website: directory under maintenance (#6571)" This reverts commit 8e5481611b77971f08f8434b3e9c7cc61d617e3b. --- website/src/directory.html | 1 - website/src/index.html | 1 - 2 files changed, 2 deletions(-) diff --git a/website/src/directory.html b/website/src/directory.html index 0e2390b591..0235583ece 100644 --- a/website/src/directory.html +++ b/website/src/directory.html @@ -260,7 +260,6 @@ active_directory: true app.

SimpleX Directory is also available as a SimpleX chat bot.

Read about how to add your community.

-

Under maintenance — you can't join these groups until 14:00 UTC, 01/13.

From f99e8da8fee7a784a1dace2416e6426a27ca2213 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Tue, 13 Jan 2026 19:13:51 +0000 Subject: [PATCH 16/73] update simplexmq --- cabal.project | 2 +- scripts/nix/sha256map.nix | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cabal.project b/cabal.project index 0756146fe0..5ad3e1abcf 100644 --- a/cabal.project +++ b/cabal.project @@ -12,7 +12,7 @@ constraints: zip +disable-bzip2 +disable-zstd source-repository-package type: git location: https://github.com/simplex-chat/simplexmq.git - tag: 6aadcf1f3fc19cbc0c8be457556fbaaffb0bfc46 + tag: 1000107259a448adc93364dcbea47059dd28f26a source-repository-package type: git diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index 8f77a6505e..3a8f2ae760 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,5 +1,5 @@ { - "https://github.com/simplex-chat/simplexmq.git"."6aadcf1f3fc19cbc0c8be457556fbaaffb0bfc46" = "1qlm542jnik48zid3zy7iys7ybjmlmj3mjhc5aplfk410a5qsb93"; + "https://github.com/simplex-chat/simplexmq.git"."1000107259a448adc93364dcbea47059dd28f26a" = "00bz0jy34p1v7p5b4g73gna8pa3m2rbi0ly682l2rk8q74dsvrrh"; "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"; From 2ecee42a40ea7a62dfe34883ac78083186a1d38a Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Tue, 13 Jan 2026 22:28:52 +0000 Subject: [PATCH 17/73] core: 6.5.0.8 (simplexmq 6.5.0.7) --- cabal.project | 2 +- scripts/nix/sha256map.nix | 2 +- simplex-chat.cabal | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cabal.project b/cabal.project index 5ad3e1abcf..2f478dbc0f 100644 --- a/cabal.project +++ b/cabal.project @@ -12,7 +12,7 @@ constraints: zip +disable-bzip2 +disable-zstd source-repository-package type: git location: https://github.com/simplex-chat/simplexmq.git - tag: 1000107259a448adc93364dcbea47059dd28f26a + tag: 58212c421aa6abb6ad894b8231d8a380849b704b source-repository-package type: git diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index 3a8f2ae760..f080cb1118 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,5 +1,5 @@ { - "https://github.com/simplex-chat/simplexmq.git"."1000107259a448adc93364dcbea47059dd28f26a" = "00bz0jy34p1v7p5b4g73gna8pa3m2rbi0ly682l2rk8q74dsvrrh"; + "https://github.com/simplex-chat/simplexmq.git"."58212c421aa6abb6ad894b8231d8a380849b704b" = "1awgvhqfi7gv3xl10h21a6w2hhqc48pq6yq4f83awg1zxkh3hiqn"; "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 c021023e49..93186c8bca 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -5,7 +5,7 @@ cabal-version: 1.12 -- see: https://github.com/sol/hpack name: simplex-chat -version: 6.5.0.7 +version: 6.5.0.8 category: Web, System, Services, Cryptography homepage: https://github.com/simplex-chat/simplex-chat#readme author: simplex.chat From 56bda03c33f7c69dca26a0c43e200db335ec115c Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Wed, 14 Jan 2026 11:56:00 +0000 Subject: [PATCH 18/73] ios: 6.5-beta.4 (build 319) --- apps/ios/SimpleX.xcodeproj/project.pbxproj | 36 +++++++++++----------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index fa9a4efdf7..bc06ab4f34 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -178,8 +178,8 @@ 64C3B0212A0D359700E19930 /* CustomTimePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64C3B0202A0D359700E19930 /* CustomTimePicker.swift */; }; 64C8299D2D54AEEE006B9E89 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C829982D54AEED006B9E89 /* libgmp.a */; }; 64C8299E2D54AEEE006B9E89 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C829992D54AEEE006B9E89 /* libffi.a */; }; - 64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.7-CDRaHJn7uof5tglscSjQL5-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.7-CDRaHJn7uof5tglscSjQL5-ghc9.6.3.a */; }; - 64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.7-CDRaHJn7uof5tglscSjQL5.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.7-CDRaHJn7uof5tglscSjQL5.a */; }; + 64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.8-6ZoDNN7rGWGJ2cn2Wz03rA-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.8-6ZoDNN7rGWGJ2cn2Wz03rA-ghc9.6.3.a */; }; + 64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.8-6ZoDNN7rGWGJ2cn2Wz03rA.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.8-6ZoDNN7rGWGJ2cn2Wz03rA.a */; }; 64C829A12D54AEEE006B9E89 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299C2D54AEEE006B9E89 /* libgmpxx.a */; }; 64D0C2C029F9688300B38D5F /* UserAddressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D0C2BF29F9688300B38D5F /* UserAddressView.swift */; }; 64D0C2C229FA57AB00B38D5F /* UserAddressLearnMore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D0C2C129FA57AB00B38D5F /* UserAddressLearnMore.swift */; }; @@ -545,8 +545,8 @@ 64C3B0202A0D359700E19930 /* CustomTimePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomTimePicker.swift; sourceTree = ""; }; 64C829982D54AEED006B9E89 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = ""; }; 64C829992D54AEEE006B9E89 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; }; - 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.7-CDRaHJn7uof5tglscSjQL5-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.5.0.7-CDRaHJn7uof5tglscSjQL5-ghc9.6.3.a"; sourceTree = ""; }; - 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.7-CDRaHJn7uof5tglscSjQL5.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.5.0.7-CDRaHJn7uof5tglscSjQL5.a"; sourceTree = ""; }; + 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.8-6ZoDNN7rGWGJ2cn2Wz03rA-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.5.0.8-6ZoDNN7rGWGJ2cn2Wz03rA-ghc9.6.3.a"; sourceTree = ""; }; + 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.8-6ZoDNN7rGWGJ2cn2Wz03rA.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.5.0.8-6ZoDNN7rGWGJ2cn2Wz03rA.a"; sourceTree = ""; }; 64C8299C2D54AEEE006B9E89 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = ""; }; 64D0C2BF29F9688300B38D5F /* UserAddressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAddressView.swift; sourceTree = ""; }; 64D0C2C129FA57AB00B38D5F /* UserAddressLearnMore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAddressLearnMore.swift; sourceTree = ""; }; @@ -708,8 +708,8 @@ 64C8299D2D54AEEE006B9E89 /* libgmp.a in Frameworks */, 64C8299E2D54AEEE006B9E89 /* libffi.a in Frameworks */, 64C829A12D54AEEE006B9E89 /* libgmpxx.a in Frameworks */, - 64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.7-CDRaHJn7uof5tglscSjQL5-ghc9.6.3.a in Frameworks */, - 64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.7-CDRaHJn7uof5tglscSjQL5.a in Frameworks */, + 64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.8-6ZoDNN7rGWGJ2cn2Wz03rA-ghc9.6.3.a in Frameworks */, + 64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.8-6ZoDNN7rGWGJ2cn2Wz03rA.a in Frameworks */, CE38A29C2C3FCD72005ED185 /* SwiftyGif in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -795,8 +795,8 @@ 64C829992D54AEEE006B9E89 /* libffi.a */, 64C829982D54AEED006B9E89 /* libgmp.a */, 64C8299C2D54AEEE006B9E89 /* libgmpxx.a */, - 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.7-CDRaHJn7uof5tglscSjQL5-ghc9.6.3.a */, - 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.7-CDRaHJn7uof5tglscSjQL5.a */, + 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.8-6ZoDNN7rGWGJ2cn2Wz03rA-ghc9.6.3.a */, + 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.8-6ZoDNN7rGWGJ2cn2Wz03rA.a */, ); path = Libraries; sourceTree = ""; @@ -2003,7 +2003,7 @@ CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 318; + CURRENT_PROJECT_VERSION = 319; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; @@ -2053,7 +2053,7 @@ CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 318; + CURRENT_PROJECT_VERSION = 319; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; @@ -2095,7 +2095,7 @@ buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 318; + CURRENT_PROJECT_VERSION = 319; DEVELOPMENT_TEAM = 5NN7GUYB6T; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 15.0; @@ -2115,7 +2115,7 @@ buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 318; + CURRENT_PROJECT_VERSION = 319; DEVELOPMENT_TEAM = 5NN7GUYB6T; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 15.0; @@ -2140,7 +2140,7 @@ CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 318; + CURRENT_PROJECT_VERSION = 319; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; GCC_OPTIMIZATION_LEVEL = s; @@ -2177,7 +2177,7 @@ CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 318; + CURRENT_PROJECT_VERSION = 319; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; ENABLE_CODE_COVERAGE = NO; @@ -2214,7 +2214,7 @@ CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES; CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 318; + CURRENT_PROJECT_VERSION = 319; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; DYLIB_COMPATIBILITY_VERSION = 1; @@ -2265,7 +2265,7 @@ CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES; CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 318; + CURRENT_PROJECT_VERSION = 319; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; DYLIB_COMPATIBILITY_VERSION = 1; @@ -2316,7 +2316,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CODE_SIGN_ENTITLEMENTS = "SimpleX SE/SimpleX SE.entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 318; + CURRENT_PROJECT_VERSION = 319; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; @@ -2350,7 +2350,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CODE_SIGN_ENTITLEMENTS = "SimpleX SE/SimpleX SE.entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 318; + CURRENT_PROJECT_VERSION = 319; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; From b2cbe9a41eab5872cf7cc0470e8b0acddeb6eda0 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Wed, 14 Jan 2026 12:34:31 +0000 Subject: [PATCH 19/73] 6.5-beta.4: android 332, desktop 129 --- apps/multiplatform/gradle.properties | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/multiplatform/gradle.properties b/apps/multiplatform/gradle.properties index 38ef04daaa..167a376e82 100644 --- a/apps/multiplatform/gradle.properties +++ b/apps/multiplatform/gradle.properties @@ -24,13 +24,13 @@ android.nonTransitiveRClass=true kotlin.mpp.androidSourceSetLayoutVersion=2 kotlin.jvm.target=11 -android.version_name=6.5-beta.3 -android.version_code=331 +android.version_name=6.5-beta.4 +android.version_code=332 android.bundle=false -desktop.version_name=6.5-beta.3 -desktop.version_code=128 +desktop.version_name=6.5-beta.4 +desktop.version_code=129 kotlin.version=2.1.20 gradle.plugin.version=8.7.0 From 43aa3e7e8ad9f216c23c2490b7ee77b7161530fb Mon Sep 17 00:00:00 2001 From: Evgeny Date: Wed, 14 Jan 2026 21:42:21 +0000 Subject: [PATCH 20/73] nodejs: simplex-chat npm package (#5556) * nodejs: addon * rename * changes * change lib name * package * lib path * simplex-chat-nodejs: fix library paths * simplex-chat-nodejs: change addon name * simplex-chat-nodejs: install libs, adjust package and installation * simplex-chat-nodejs: add npmignore * gitignore: add additional nodejs path * simplex-chat-nodejs: fix shim name * gitignore: ignore nodejs package lock * simplex-chat-nodejs: rename shim to underscore * simplex-chat-nodejs: fix library loading on Mac * simplex-chat-nodejs: expose low-level functions, move tests * simplex-chat-nodejs: expose shim fucntions * simplex-chat-nodejs: fixed libs version * simplex-chat-nodejs: switch to official repository * simpelx-chat-nodejs: adjust release tag * async addon, tests * refactor, fixes * high level chat api * simplify cpp add-on - move logic to JS, fix API * api for events, api test * update @simplex-chat/types * Revert "update @simplex-chat/types" This reverts commit da3f89866f01350db4ad265c4fe59ac45fa68b55. * change @simplex-chat/types version * receiver for any events, wait with timeout * low-level bot example * typedoc * network connection events * declarative bot api * readme, docs * update docs * update readme * add liveMessage support * allow passing welcome message as string * @simplex-chat/webrtc-client 6.5.0-beta.3 * bot test * concurrent connection in tests * nodejs/download-libs: cleanup on version mismatch * nodejs/download-libs: bump libs version * do not handle signals in Haskell * update bot examples * flatten docs and use local links to code * update readme * 6.5.0-beta.4 * include more files in npm package, 6.5.0-beta.4.2 * .gitignore --------- Co-authored-by: Avently <7953703+avently@users.noreply.github.com> Co-authored-by: shum --- .gitignore | 1 + bots/api/EVENTS.md | 47 + bots/api/TYPES.md | 21 + bots/src/API/Docs/Events.hs | 11 +- bots/src/API/Docs/Types.hs | 3 + bots/src/API/TypeInfo.hs | 2 + .../types/typescript/README.md | 2 +- .../types/typescript/package.json | 4 +- .../types/typescript/src/events.ts | 25 + .../types/typescript/src/types.ts | 31 + .../simplex-chat-client/typescript/README.md | 8 +- .../typescript/package.json | 8 +- .../typescript/tests/client.test.ts | 2 +- packages/simplex-chat-nodejs/.gitignore | 8 + packages/simplex-chat-nodejs/.npmignore | 3 + packages/simplex-chat-nodejs/LICENSE | 661 +++++++ packages/simplex-chat-nodejs/README.md | 90 + packages/simplex-chat-nodejs/binding.gyp | 35 + packages/simplex-chat-nodejs/cpp/simplex.cc | 414 +++++ packages/simplex-chat-nodejs/cpp/simplex.h | 46 + .../simplex-chat-nodejs/docs/Namespace.api.md | 32 + .../simplex-chat-nodejs/docs/Namespace.bot.md | 20 + .../docs/Namespace.core.md | 48 + .../docs/Namespace.util.md | 25 + packages/simplex-chat-nodejs/docs/README.md | 12 + .../docs/api.Class.ChatApi.md | 1602 +++++++++++++++++ .../docs/api.Class.ChatCommandError.md | 205 +++ .../docs/api.Enumeration.ConnReqType.md | 27 + .../docs/api.Interface.BotAddressSettings.md | 60 + .../docs/api.TypeAlias.EventSubscriberFunc.md | 27 + .../docs/api.TypeAlias.EventSubscribers.md | 11 + .../api.Variable.defaultBotAddressSettings.md | 11 + .../docs/bot.Function.run.md | 21 + .../docs/bot.Interface.BotConfig.md | 75 + .../docs/bot.Interface.BotDbOpts.md | 33 + .../docs/bot.Interface.BotOptions.md | 81 + .../docs/core.Class.ChatAPIError.md | 205 +++ .../docs/core.Class.ChatInitError.md | 205 +++ ...MigrationError.Interface.ErrorMigration.md | 41 + ...rationError.Interface.ErrorNotADatabase.md | 33 + ...ore.DBMigrationError.Interface.ErrorSQL.md | 41 + ...tionError.Interface.InvalidConfirmation.md | 25 + .../core.DBMigrationError.TypeAlias.Tag.md | 11 + .../core.Enumeration.MigrationConfirmation.md | 43 + .../docs/core.Function.chatCloseStore.md | 23 + .../docs/core.Function.chatDecryptFile.md | 31 + .../docs/core.Function.chatEncryptFile.md | 31 + .../docs/core.Function.chatMigrateInit.md | 31 + .../docs/core.Function.chatReadFile.md | 27 + .../docs/core.Function.chatRecvMsgWait.md | 27 + .../docs/core.Function.chatSendCmd.md | 27 + .../docs/core.Function.chatWriteFile.md | 31 + .../docs/core.Interface.APIResult.md | 31 + .../docs/core.Interface.CryptoArgs.md | 27 + .../docs/core.Interface.UpMigration.md | 25 + .../core.MTRError.Interface.MTREDifferent.md | 33 + .../core.MTRError.Interface.MTRENoDown.md | 33 + .../docs/core.MTRError.TypeAlias.Tag.md | 11 + ...re.MigrationError.Interface.MEDowngrade.md | 33 + ...core.MigrationError.Interface.MEUpgrade.md | 33 + ...MigrationError.Interface.MigrationError.md | 33 + .../docs/core.MigrationError.TypeAlias.Tag.md | 11 + .../docs/core.Namespace.DBMigrationError.md | 18 + .../docs/core.Namespace.MTRError.md | 16 + .../docs/core.Namespace.MigrationError.md | 17 + .../docs/core.TypeAlias.DBMigrationError.md | 11 + .../docs/core.TypeAlias.MTRError.md | 11 + .../docs/core.TypeAlias.MigrationError.md | 11 + .../docs/util.Function.botAddressSettings.md | 21 + .../docs/util.Function.chatInfoName.md | 21 + .../docs/util.Function.chatInfoRef.md | 21 + .../docs/util.Function.ciBotCommand.md | 21 + .../docs/util.Function.ciContentText.md | 21 + .../docs/util.Function.contactAddressStr.md | 21 + .../docs/util.Function.fromLocalProfile.md | 21 + .../docs/util.Function.reactionText.md | 21 + .../docs/util.Function.senderName.md | 25 + .../docs/util.Interface.BotCommand.md | 25 + .../examples/squaring-bot-readme.js | 17 + .../examples/squaring-bot.ts | 42 + packages/simplex-chat-nodejs/jest.config.js | 10 + packages/simplex-chat-nodejs/package.json | 58 + packages/simplex-chat-nodejs/src/api.ts | 834 +++++++++ packages/simplex-chat-nodejs/src/bot.ts | 216 +++ packages/simplex-chat-nodejs/src/core.ts | 210 +++ .../simplex-chat-nodejs/src/download-libs.js | 224 +++ packages/simplex-chat-nodejs/src/index.ts | 22 + packages/simplex-chat-nodejs/src/simplex.d.ts | 10 + packages/simplex-chat-nodejs/src/simplex.js | 1 + packages/simplex-chat-nodejs/src/util.ts | 92 + .../simplex-chat-nodejs/tests/api.test.ts | 67 + .../simplex-chat-nodejs/tests/bot.test.ts | 60 + .../simplex-chat-nodejs/tests/core.test.ts | 85 + .../simplex-chat-nodejs/tests/tsconfig.json | 7 + packages/simplex-chat-nodejs/tsconfig.json | 23 + packages/simplex-chat-nodejs/typedoc.json | 14 + 96 files changed, 7095 insertions(+), 13 deletions(-) create mode 100644 packages/simplex-chat-nodejs/.gitignore create mode 100644 packages/simplex-chat-nodejs/.npmignore create mode 100644 packages/simplex-chat-nodejs/LICENSE create mode 100644 packages/simplex-chat-nodejs/README.md create mode 100644 packages/simplex-chat-nodejs/binding.gyp create mode 100644 packages/simplex-chat-nodejs/cpp/simplex.cc create mode 100644 packages/simplex-chat-nodejs/cpp/simplex.h create mode 100644 packages/simplex-chat-nodejs/docs/Namespace.api.md create mode 100644 packages/simplex-chat-nodejs/docs/Namespace.bot.md create mode 100644 packages/simplex-chat-nodejs/docs/Namespace.core.md create mode 100644 packages/simplex-chat-nodejs/docs/Namespace.util.md create mode 100644 packages/simplex-chat-nodejs/docs/README.md create mode 100644 packages/simplex-chat-nodejs/docs/api.Class.ChatApi.md create mode 100644 packages/simplex-chat-nodejs/docs/api.Class.ChatCommandError.md create mode 100644 packages/simplex-chat-nodejs/docs/api.Enumeration.ConnReqType.md create mode 100644 packages/simplex-chat-nodejs/docs/api.Interface.BotAddressSettings.md create mode 100644 packages/simplex-chat-nodejs/docs/api.TypeAlias.EventSubscriberFunc.md create mode 100644 packages/simplex-chat-nodejs/docs/api.TypeAlias.EventSubscribers.md create mode 100644 packages/simplex-chat-nodejs/docs/api.Variable.defaultBotAddressSettings.md create mode 100644 packages/simplex-chat-nodejs/docs/bot.Function.run.md create mode 100644 packages/simplex-chat-nodejs/docs/bot.Interface.BotConfig.md create mode 100644 packages/simplex-chat-nodejs/docs/bot.Interface.BotDbOpts.md create mode 100644 packages/simplex-chat-nodejs/docs/bot.Interface.BotOptions.md create mode 100644 packages/simplex-chat-nodejs/docs/core.Class.ChatAPIError.md create mode 100644 packages/simplex-chat-nodejs/docs/core.Class.ChatInitError.md create mode 100644 packages/simplex-chat-nodejs/docs/core.DBMigrationError.Interface.ErrorMigration.md create mode 100644 packages/simplex-chat-nodejs/docs/core.DBMigrationError.Interface.ErrorNotADatabase.md create mode 100644 packages/simplex-chat-nodejs/docs/core.DBMigrationError.Interface.ErrorSQL.md create mode 100644 packages/simplex-chat-nodejs/docs/core.DBMigrationError.Interface.InvalidConfirmation.md create mode 100644 packages/simplex-chat-nodejs/docs/core.DBMigrationError.TypeAlias.Tag.md create mode 100644 packages/simplex-chat-nodejs/docs/core.Enumeration.MigrationConfirmation.md create mode 100644 packages/simplex-chat-nodejs/docs/core.Function.chatCloseStore.md create mode 100644 packages/simplex-chat-nodejs/docs/core.Function.chatDecryptFile.md create mode 100644 packages/simplex-chat-nodejs/docs/core.Function.chatEncryptFile.md create mode 100644 packages/simplex-chat-nodejs/docs/core.Function.chatMigrateInit.md create mode 100644 packages/simplex-chat-nodejs/docs/core.Function.chatReadFile.md create mode 100644 packages/simplex-chat-nodejs/docs/core.Function.chatRecvMsgWait.md create mode 100644 packages/simplex-chat-nodejs/docs/core.Function.chatSendCmd.md create mode 100644 packages/simplex-chat-nodejs/docs/core.Function.chatWriteFile.md create mode 100644 packages/simplex-chat-nodejs/docs/core.Interface.APIResult.md create mode 100644 packages/simplex-chat-nodejs/docs/core.Interface.CryptoArgs.md create mode 100644 packages/simplex-chat-nodejs/docs/core.Interface.UpMigration.md create mode 100644 packages/simplex-chat-nodejs/docs/core.MTRError.Interface.MTREDifferent.md create mode 100644 packages/simplex-chat-nodejs/docs/core.MTRError.Interface.MTRENoDown.md create mode 100644 packages/simplex-chat-nodejs/docs/core.MTRError.TypeAlias.Tag.md create mode 100644 packages/simplex-chat-nodejs/docs/core.MigrationError.Interface.MEDowngrade.md create mode 100644 packages/simplex-chat-nodejs/docs/core.MigrationError.Interface.MEUpgrade.md create mode 100644 packages/simplex-chat-nodejs/docs/core.MigrationError.Interface.MigrationError.md create mode 100644 packages/simplex-chat-nodejs/docs/core.MigrationError.TypeAlias.Tag.md create mode 100644 packages/simplex-chat-nodejs/docs/core.Namespace.DBMigrationError.md create mode 100644 packages/simplex-chat-nodejs/docs/core.Namespace.MTRError.md create mode 100644 packages/simplex-chat-nodejs/docs/core.Namespace.MigrationError.md create mode 100644 packages/simplex-chat-nodejs/docs/core.TypeAlias.DBMigrationError.md create mode 100644 packages/simplex-chat-nodejs/docs/core.TypeAlias.MTRError.md create mode 100644 packages/simplex-chat-nodejs/docs/core.TypeAlias.MigrationError.md create mode 100644 packages/simplex-chat-nodejs/docs/util.Function.botAddressSettings.md create mode 100644 packages/simplex-chat-nodejs/docs/util.Function.chatInfoName.md create mode 100644 packages/simplex-chat-nodejs/docs/util.Function.chatInfoRef.md create mode 100644 packages/simplex-chat-nodejs/docs/util.Function.ciBotCommand.md create mode 100644 packages/simplex-chat-nodejs/docs/util.Function.ciContentText.md create mode 100644 packages/simplex-chat-nodejs/docs/util.Function.contactAddressStr.md create mode 100644 packages/simplex-chat-nodejs/docs/util.Function.fromLocalProfile.md create mode 100644 packages/simplex-chat-nodejs/docs/util.Function.reactionText.md create mode 100644 packages/simplex-chat-nodejs/docs/util.Function.senderName.md create mode 100644 packages/simplex-chat-nodejs/docs/util.Interface.BotCommand.md create mode 100644 packages/simplex-chat-nodejs/examples/squaring-bot-readme.js create mode 100644 packages/simplex-chat-nodejs/examples/squaring-bot.ts create mode 100644 packages/simplex-chat-nodejs/jest.config.js create mode 100644 packages/simplex-chat-nodejs/package.json create mode 100644 packages/simplex-chat-nodejs/src/api.ts create mode 100644 packages/simplex-chat-nodejs/src/bot.ts create mode 100644 packages/simplex-chat-nodejs/src/core.ts create mode 100644 packages/simplex-chat-nodejs/src/download-libs.js create mode 100644 packages/simplex-chat-nodejs/src/index.ts create mode 100644 packages/simplex-chat-nodejs/src/simplex.d.ts create mode 100644 packages/simplex-chat-nodejs/src/simplex.js create mode 100644 packages/simplex-chat-nodejs/src/util.ts create mode 100644 packages/simplex-chat-nodejs/tests/api.test.ts create mode 100644 packages/simplex-chat-nodejs/tests/bot.test.ts create mode 100644 packages/simplex-chat-nodejs/tests/core.test.ts create mode 100644 packages/simplex-chat-nodejs/tests/tsconfig.json create mode 100644 packages/simplex-chat-nodejs/tsconfig.json create mode 100644 packages/simplex-chat-nodejs/typedoc.json diff --git a/.gitignore b/.gitignore index bf565453a5..929bda7250 100644 --- a/.gitignore +++ b/.gitignore @@ -81,3 +81,4 @@ website/.cache website/test/stubs-layout-cache/_includes/*.js apps/android/app/release apps/multiplatform/.kotlin/sessions + diff --git a/bots/api/EVENTS.md b/bots/api/EVENTS.md index 84f59e7405..d7405ef846 100644 --- a/bots/api/EVENTS.md +++ b/bots/api/EVENTS.md @@ -62,6 +62,11 @@ This file is generated automatically. - [SentGroupInvitation](#sentgroupinvitation) - [GroupLinkConnecting](#grouplinkconnecting) +[Network connection events](#network-connection-events) +- [HostConnected](#hostconnected) +- [HostDisconnected](#hostdisconnected) +- [SubscriptionStatus](#subscriptionstatus) + [Error events](#error-events) - [MessageError](#messageerror) - [ChatError](#chaterror) @@ -685,6 +690,48 @@ Sent when bot joins group via another user link. --- +## Network connection events + + + + +### HostConnected + +Messaging or file server connected + +**Record type**: +- type: "hostConnected" +- protocol: string +- transportHost: string + +--- + + +### HostDisconnected + +Messaging or file server disconnected + +**Record type**: +- type: "hostDisconnected" +- protocol: string +- transportHost: string + +--- + + +### SubscriptionStatus + +Messaging subscription status changed + +**Record type**: +- type: "subscriptionStatus" +- server: string +- subscriptionStatus: [SubscriptionStatus](./TYPES.md#subscriptionstatus) +- connections: [string] + +--- + + ## Error events Bots may log these events for debugging. There will be many error events - this does NOT indicate a malfunction - e.g., they may happen because of bad network connectivity, or because messages may be delivered to deleted chats for a short period of time (they will be ignored). diff --git a/bots/api/TYPES.md b/bots/api/TYPES.md index 1aaa291204..cded13f3a2 100644 --- a/bots/api/TYPES.md +++ b/bots/api/TYPES.md @@ -153,6 +153,7 @@ This file is generated automatically. - [SndGroupEvent](#sndgroupevent) - [SrvError](#srverror) - [StoreError](#storeerror) +- [SubscriptionStatus](#subscriptionstatus) - [SwitchPhase](#switchphase) - [TimedMessagesGroupPreference](#timedmessagesgrouppreference) - [TimedMessagesPreference](#timedmessagespreference) @@ -3593,6 +3594,26 @@ WorkItemError: - errContext: string +--- + +## SubscriptionStatus + +**Discriminated union type**: + +Active: +- type: "active" + +Pending: +- type: "pending" + +Removed: +- type: "removed" +- subError: string + +NoSub: +- type: "noSub" + + --- ## SwitchPhase diff --git a/bots/src/API/Docs/Events.hs b/bots/src/API/Docs/Events.hs index ecb8a5ba04..130ea89846 100644 --- a/bots/src/API/Docs/Events.hs +++ b/bots/src/API/Docs/Events.hs @@ -136,6 +136,14 @@ chatEventsDocsData = ], [] ), + ( "Network connection events", + "", + [ ("CEvtHostConnected", "Messaging or file server connected"), + ("CEvtHostDisconnected", "Messaging or file server disconnected"), + ("CEvtSubscriptionStatus", "Messaging subscription status changed") + ], + [] + ), ( "Error events", "Bots may log these events for debugging. \ \There will be many error events - this does NOT indicate a malfunction - \ @@ -178,9 +186,6 @@ undocumentedEvents = "CEvtCustomChatEvent", "CEvtGroupMemberRatchetSync", "CEvtGroupMemberSwitch", - "CEvtHostConnected", - "CEvtHostDisconnected", - "CEvtSubscriptionStatus", "CEvtNewRemoteHost", "CEvtNoMemberContactCreating", "CEvtNtfMessage", diff --git a/bots/src/API/Docs/Types.hs b/bots/src/API/Docs/Types.hs index 73e427fa03..73ad90e91b 100644 --- a/bots/src/API/Docs/Types.hs +++ b/bots/src/API/Docs/Types.hs @@ -176,6 +176,7 @@ ciQuoteType = updateRecord (RecordTypeInfo name fields) = RecordTypeInfo name $ map optChatDir fields in st {recordTypes = map updateRecord records} -- need to map even though there is one constructor in this type +-- type info, JSON encoding, constructor prefix, removed constructors, string encoding for commands, description chatTypesDocsData :: [(SumTypeInfo, SumTypeJsonEncoding, String, [ConsName], Expr, Text)] chatTypesDocsData = [ ((sti @(Chat 'CTDirect)) {typeName = "AChat"}, STRecord, "", [], "", ""), @@ -332,6 +333,7 @@ chatTypesDocsData = (sti @SndGroupEvent, STUnion, "SGE", [], "", ""), (sti @SrvError, STUnion, "SrvErr", [], "", ""), (sti @StoreError, STUnion, "SE", [], "", ""), + (sti @SubscriptionStatus, STUnion, "SS", [], "", ""), (sti @SwitchPhase, STEnum, "SP", [], "", ""), (sti @TimedMessagesGroupPreference, STRecord, "", [], "", ""), (sti @TimedMessagesPreference, STRecord, "", [], "", ""), @@ -522,6 +524,7 @@ deriving instance Generic SndFileTransfer deriving instance Generic SndGroupEvent deriving instance Generic SrvError deriving instance Generic StoreError +deriving instance Generic SubscriptionStatus deriving instance Generic SwitchPhase deriving instance Generic TimedMessagesGroupPreference deriving instance Generic TimedMessagesPreference diff --git a/bots/src/API/TypeInfo.hs b/bots/src/API/TypeInfo.hs index df43374ffa..a70de72d01 100644 --- a/bots/src/API/TypeInfo.hs +++ b/bots/src/API/TypeInfo.hs @@ -194,6 +194,7 @@ toTypeInfo tr = primitiveToLower st@(ST t ps) = let t' = fstToLower t in if t' `elem` primitiveTypes then ST t' ps else st stringTypes = [ "AConnectionLink", + "AProtocolType", "AgentConnId", "AgentInvId", "AgentRcvFileId", @@ -212,6 +213,7 @@ toTypeInfo tr = "ProtocolServer", "SbKey", "SharedMsgId", + "TransportHost", "UIColor", "UserPwd", "XContactId" diff --git a/packages/simplex-chat-client/types/typescript/README.md b/packages/simplex-chat-client/types/typescript/README.md index ad13a5d76e..e30cf0d0c3 100644 --- a/packages/simplex-chat-client/types/typescript/README.md +++ b/packages/simplex-chat-client/types/typescript/README.md @@ -2,7 +2,7 @@ This TypeScript library provides auto-generated types for bots API: commands and responses, events and all types they use. -It is used in [simplex-chat](https://www.npmjs.com/package/simplex-chat) library that uses WebSockets interface of SimpleX Chat CLI. +It is used in [simplex-chat](https://www.npmjs.com/package/simplex-chat) Node.js library. [API reference](https://github.com/simplex-chat/simplex-chat/tree/stable/bots). diff --git a/packages/simplex-chat-client/types/typescript/package.json b/packages/simplex-chat-client/types/typescript/package.json index c24b93fbed..a135b286c2 100644 --- a/packages/simplex-chat-client/types/typescript/package.json +++ b/packages/simplex-chat-client/types/typescript/package.json @@ -1,6 +1,6 @@ { "name": "@simplex-chat/types", - "version": "0.2.0", + "version": "0.3.0", "description": "TypeScript types for SimpleX Chat bot libraries", "main": "dist/index.js", "types": "dist/index.d.ts", @@ -35,7 +35,7 @@ "bugs": { "url": "https://github.com/simplex-chat/simplex-chat/issues" }, - "homepage": "https://github.com/simplex-chat/simplex-chat#readme", + "homepage": "https://github.com/simplex-chat/simplex-chat/tree/stable/packages/simplex-chat-client/types/typescript#readme", "dependencies": { "typescript": "^5.9.2" } diff --git a/packages/simplex-chat-client/types/typescript/src/events.ts b/packages/simplex-chat-client/types/typescript/src/events.ts index d7a0419bbe..cb6ba85c8b 100644 --- a/packages/simplex-chat-client/types/typescript/src/events.ts +++ b/packages/simplex-chat-client/types/typescript/src/events.ts @@ -46,6 +46,9 @@ export type ChatEvent = | CEvt.JoinedGroupMemberConnecting | CEvt.SentGroupInvitation | CEvt.GroupLinkConnecting + | CEvt.HostConnected + | CEvt.HostDisconnected + | CEvt.SubscriptionStatus | CEvt.MessageError | CEvt.ChatError | CEvt.ChatErrors @@ -94,6 +97,9 @@ export namespace CEvt { | "joinedGroupMemberConnecting" | "sentGroupInvitation" | "groupLinkConnecting" + | "hostConnected" + | "hostDisconnected" + | "subscriptionStatus" | "messageError" | "chatError" | "chatErrors" @@ -411,6 +417,25 @@ export namespace CEvt { hostMember: T.GroupMember } + export interface HostConnected extends Interface { + type: "hostConnected" + protocol: string + transportHost: string + } + + export interface HostDisconnected extends Interface { + type: "hostDisconnected" + protocol: string + transportHost: string + } + + export interface SubscriptionStatus extends Interface { + type: "subscriptionStatus" + server: string + subscriptionStatus: T.SubscriptionStatus + connections: string[] + } + export interface MessageError extends Interface { type: "messageError" user: T.User diff --git a/packages/simplex-chat-client/types/typescript/src/types.ts b/packages/simplex-chat-client/types/typescript/src/types.ts index bca0179a91..c4371082cc 100644 --- a/packages/simplex-chat-client/types/typescript/src/types.ts +++ b/packages/simplex-chat-client/types/typescript/src/types.ts @@ -4299,6 +4299,37 @@ export namespace StoreError { } } +export type SubscriptionStatus = + | SubscriptionStatus.Active + | SubscriptionStatus.Pending + | SubscriptionStatus.Removed + | SubscriptionStatus.NoSub + +export namespace SubscriptionStatus { + export type Tag = "active" | "pending" | "removed" | "noSub" + + interface Interface { + type: Tag + } + + export interface Active extends Interface { + type: "active" + } + + export interface Pending extends Interface { + type: "pending" + } + + export interface Removed extends Interface { + type: "removed" + subError: string + } + + export interface NoSub extends Interface { + type: "noSub" + } +} + export enum SwitchPhase { Started = "started", Confirmed = "confirmed", diff --git a/packages/simplex-chat-client/typescript/README.md b/packages/simplex-chat-client/typescript/README.md index c1756dc82c..a67e822de2 100644 --- a/packages/simplex-chat-client/typescript/README.md +++ b/packages/simplex-chat-client/typescript/README.md @@ -1,4 +1,8 @@ -# SimpleX Chat JavaScript client +# SimpleX Chat JavaScript WebRTC client + +**THIS PACKAGE IS DEPRECATED** + +Use [SimpleX Chat Node.js library](https://github.com/simplex-chat/simplex-chat/tree/stable/packages/simplex-chat-nodejs#readme) instead of this package. This is a TypeScript library that defines WebSocket API client for [SimpleX Chat terminal CLI](https://github.com/simplex-chat/simplex-chat/blob/stable/docs/CLI.md) that should be run as a WebSockets server on any port: @@ -24,7 +28,7 @@ Please share your use cases and implementations. ## Quick start ``` -npm i simplex-chat +npm i @simplex-chat/webrtc-client@6.5.0-beta.3 npm run build ``` diff --git a/packages/simplex-chat-client/typescript/package.json b/packages/simplex-chat-client/typescript/package.json index b721dbcce2..c9d6165336 100644 --- a/packages/simplex-chat-client/typescript/package.json +++ b/packages/simplex-chat-client/typescript/package.json @@ -1,6 +1,6 @@ { - "name": "simplex-chat", - "version": "0.3.0", + "name": "@simplex-chat/webrtc-client", + "version": "6.5.0-beta.3", "description": "SimpleX Chat client", "main": "dist/index.js", "types": "dist/index.d.ts", @@ -38,12 +38,12 @@ "bugs": { "url": "https://github.com/simplex-chat/simplex-chat/issues" }, - "homepage": "https://github.com/simplex-chat/simplex-chat/packages/simplex-chat-client/typescript#readme", + "homepage": "https://github.com/simplex-chat/simplex-chat/tree/stable/packages/simplex-chat-client/typescript#readme", "dependencies": { + "@simplex-chat/types": "^0.3.0", "isomorphic-ws": "^4.0.1" }, "devDependencies": { - "@simplex-chat/types": "^0.1.0", "@types/jest": "^27.5.1", "@types/node": "^18.11.18", "@typescript-eslint/eslint-plugin": "^5.23.0", diff --git a/packages/simplex-chat-client/typescript/tests/client.test.ts b/packages/simplex-chat-client/typescript/tests/client.test.ts index 9cd0a365f1..1c5c4d4baa 100644 --- a/packages/simplex-chat-client/typescript/tests/client.test.ts +++ b/packages/simplex-chat-client/typescript/tests/client.test.ts @@ -24,7 +24,7 @@ describe.skip("ChatClient (expects SimpleX Chat server with a user, without cont const r2 = await c.msgQ.dequeue() assert.strictEqual(r1.type, "contactConnecting") assert.strictEqual(r2.type, "contactConnected") - const contact1 = (r1 as CEvt.ContactConnected).contact + const contact1 = (r1 as CEvt.ContactConnecting).contact // const contact2 = (r2 as C.CRContactConnected).contact const r3 = await c.apiSendTextMessage(T.ChatType.Direct, contact1.contactId, "hello") assert(r3[0].chatItem.content.type === "sndMsgContent" && r3[0].chatItem.content.msgContent.text === "hello") diff --git a/packages/simplex-chat-nodejs/.gitignore b/packages/simplex-chat-nodejs/.gitignore new file mode 100644 index 0000000000..322e38bfda --- /dev/null +++ b/packages/simplex-chat-nodejs/.gitignore @@ -0,0 +1,8 @@ +node_modules/ +package-lock.json +.vscode +build/ +libs/ +dist/ +coverage/ +tmp/ diff --git a/packages/simplex-chat-nodejs/.npmignore b/packages/simplex-chat-nodejs/.npmignore new file mode 100644 index 0000000000..26fdd85dff --- /dev/null +++ b/packages/simplex-chat-nodejs/.npmignore @@ -0,0 +1,3 @@ +libs/ +build/ +node_modules/ diff --git a/packages/simplex-chat-nodejs/LICENSE b/packages/simplex-chat-nodejs/LICENSE new file mode 100644 index 0000000000..0ad25db4bd --- /dev/null +++ b/packages/simplex-chat-nodejs/LICENSE @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. diff --git a/packages/simplex-chat-nodejs/README.md b/packages/simplex-chat-nodejs/README.md new file mode 100644 index 0000000000..982b8d0553 --- /dev/null +++ b/packages/simplex-chat-nodejs/README.md @@ -0,0 +1,90 @@ +# SimpleX Chat Node.js library + +This library replaced now deprecated [SimpleX Chat WebRTC TypeScript client](https://www.npmjs.com/package/@simplex-chat/webrtc-client). + +## Use cases + +- chat bots: you can implement any logic of connecting with and communicating with SimpleX Chat users. Using chat groups a chat bot can connect SimpleX Chat users with each other. +- control of the equipment: e.g. servers or home automation. SimpleX Chat provides secure and authorised connections, so this is more secure than using rest APIs. +- any scenarios of scripted message sending. +- chat and chat-based interfaces. + +Please share your use cases and implementations. + +## Quick start: a simple bot + +``` +npm i simplex-chat@6.5.0-beta.4.2 +``` + +Simple bot that replies with squares of numbers you send to it: + +```javascript +(async () => { + const {bot} = await import("simplex-chat") + // if you are running from this GitHub repo: + // const {bot} = await import("../dist/index.js") + const [chat, _user, _address] = await bot.run({ + profile: {displayName: "Squaring bot example", fullName: ""}, + dbOpts: {dbFilePrefix: "./squaring_bot", dbKey: ""}, + options: { + addressSettings: {welcomeMessage: "If you send me a number, I will calculate its square."}, + }, + onMessage: async (ci, content) => { + const n = +content.text + const reply = typeof n === "number" && !isNaN(n) + ? `${n} * ${n} = ${n * n}` + : `this is not a number` + await chat.apiSendTextReply(ci, reply) + } + }) +})() +``` + +If you installed this package as dependency, you can run this example with: + +```sh +node ./node_modules/simplex-chat/examples/squaring-bot-readme.js` +``` + +If you cloned this repository, you can: + +``` +cd ./packages/simplex-chat-nodejs +npm install +npm run build +node ./examples/squaring-bot-readme.js +``` + +There is an example with more options in [./examples/squaring-bot.ts](./examples/squaring-bot.ts). + +You can run it with: `npx ts-node ./examples/squaring-bot.ts` + +## Documentation + +The library docs are [here](./docs/README.md). + +Library provides these modules: +- [bot](./docs/Namespace.bot.md): a simple declarative API to run a chat-bot with a single function call. It automates creating and updating of the bot profile, address and bot commands shown in the app UI. +- [api](./docs/Namespace.api.md): an API to send chat commands and receive chat events to/from chat core. You need to use it in bot event handlers, and for any other use cases. +- [core](./docs/Namespace.core.md): a low level API to the core library - the same that is used in desktop clients. You are unlikely to ever need to use this module directly. +- [util](./docs/Namespace.util.md): useful functions for chat events and types. + + +This library uses [@simplex-chat/types](https://www.npmjs.com/package/@simplex-chat/types) package with auto-generated [bot API types](../../bots/api/README.md). + +## Supported chat functions + +Library provides types and functions to: + +- create and change user profile (although, in most cases you can do it manually, via SimpleX Chat terminal app). +- create and accept invitations or connect with the contacts. +- create and manage long-term user address, accepting connection requests automatically. +- send, receive, delete and update messages, and add message reactions. +- create, join and manage group. +- send and receive files. +- etc. + +## License + +[AGPL v3](./LICENSE) diff --git a/packages/simplex-chat-nodejs/binding.gyp b/packages/simplex-chat-nodejs/binding.gyp new file mode 100644 index 0000000000..addb293f5b --- /dev/null +++ b/packages/simplex-chat-nodejs/binding.gyp @@ -0,0 +1,35 @@ +{ + "targets": [ + { + "target_name": "simplex", + "sources": [ "cpp/simplex.cc" ], + "include_dirs": [ + " +#include +#include +#include +#include +#include "simplex.h" + +namespace simplex { + +using namespace Napi; + +void haskell_init() { + int argc = 6; + const char *argv[] = { + "simplex", + "+RTS", // requires `hs_init_with_rtsopts` + "-A64m", // chunk size for new allocations + "-H64m", // initial heap size + "-xn", // non-moving GC + "--install-signal-handlers=no", + nullptr}; + char **pargv = const_cast(argv); + hs_init_with_rtsopts(&argc, &pargv); +} + +class ResultAsyncWorker : public AsyncWorker { + public: + using ExecuteFn = std::function; + using ResultProcessor = std::function; + + ResultAsyncWorker(Function& callback, ExecuteFn execute_fn, ResultProcessor result_processor = nullptr) + : AsyncWorker(callback), execute_fn_(std::move(execute_fn)), result_processor_(std::move(result_processor)) {} + + void Execute() override { + execute_fn_(this); + } + + void OnOK() override { + HandleScope scope(Env()); + if (result_processor_) { + result_processor_(this, Env()); + } else { + Callback().Call({Env().Null(), String::New(Env(), result_)}); + } + } + + void OnError(const Error& e) override { + HandleScope scope(Env()); + Callback().Call({e.Value(), Env().Undefined()}); + } + + void SetResult(std::string result) { + result_ = std::move(result); + } + + void SetWorkerError(const std::string& msg) { + SetError(msg); + } + + const std::string& GetStringResult() const { + return result_; + } + + void SetCtrl(uintptr_t ctrl) { + ctrl_ = ctrl; + } + + uintptr_t GetCtrl() const { + return ctrl_; + } + + protected: + std::string result_; + uintptr_t ctrl_ = 0; + + private: + ExecuteFn execute_fn_; + ResultProcessor result_processor_; +}; + +class BinaryAsyncWorker : public AsyncWorker { + public: + using ExecuteFn = std::function; + + BinaryAsyncWorker(Function& callback, ExecuteFn execute_fn) + : AsyncWorker(callback), execute_fn_(std::move(execute_fn)) {} + + void Execute() override { + execute_fn_(this); + } + + void OnOK() override { + HandleScope scope(Env()); + if (original_buf == nullptr || binary_len == 0) { + Callback().Call({Env().Null(), Env().Undefined()}); + return; + } + char* data_ptr = original_buf + 5; + auto finalizer = [](Napi::Env env, char* finalize_data, char* orig) { + free(orig); + }; + Napi::Buffer buffer = Napi::Buffer::New(Env(), data_ptr, binary_len, finalizer, original_buf); + Callback().Call({Env().Null(), buffer}); + } + + void OnError(const Error& e) override { + HandleScope scope(Env()); + Callback().Call({e.Value(), Env().Undefined()}); + } + + void SetWorkerError(const std::string& msg) { + SetError(msg); + } + + char* original_buf = nullptr; + size_t binary_len = 0; + + private: + ExecuteFn execute_fn_; +}; + +// Helper for converting chat_ctrl pointer to BigInt +Napi::BigInt ToChatCtrlBigInt(Napi::Env env, uintptr_t ctrl) { + return Napi::BigInt::New(env, static_cast(ctrl)); +} + +// Helper for converting BigInt to chat_ctrl pointer +chat_ctrl FromChatCtrlBigInt(const Napi::Value& value) { + Napi::Env env = value.Env(); + if (!value.IsBigInt()) { + Napi::TypeError::New(env, "Expected BigInt for ctrl").ThrowAsJavaScriptException(); + return nullptr; + } + Napi::BigInt big = value.As(); + bool lossless; + uint64_t val = big.Uint64Value(&lossless); + if (!lossless) { + Napi::TypeError::New(env, "BigInt too large for ctrl").ThrowAsJavaScriptException(); + return nullptr; + } + return reinterpret_cast(val); +} + +// Helper for handling common C result patterns (no empty check) +void HandleCResult(ResultAsyncWorker* worker, char* c_res, const std::string& func_name) { + if (c_res == nullptr) { + worker->SetWorkerError(func_name + " failed"); + return; + } + std::string res = c_res; + free(c_res); + worker->SetResult(res); +} + +Napi::Promise CreatePromiseAndCallback(Env env, Function& cb_out) { + Promise::Deferred deferred = Promise::Deferred::New(env); + cb_out = Function::New(env, [deferred](const CallbackInfo& args) { + if (!args[0].IsNull() && !args[0].IsUndefined()) { + deferred.Reject(args[0]); + } else { + deferred.Resolve(args[1]); + } + }); + return deferred.Promise(); +} + +// Common result processors +ResultAsyncWorker::ResultProcessor MigrateResultProcessor() { + return [](ResultAsyncWorker* worker, Napi::Env env) { + Napi::Array arr = Napi::Array::New(env, 2); + arr.Set(0u, ToChatCtrlBigInt(env, worker->GetCtrl())); + arr.Set(1u, Napi::String::New(env, worker->GetStringResult())); + worker->Callback().Call({env.Null(), arr}); + }; +} + +// Refactored functions using common patterns + +Value ChatMigrateInit(const CallbackInfo& args) { + Env env = args.Env(); + if (args.Length() < 3 || !args[0].IsString() || !args[1].IsString() || !args[2].IsString()) { + TypeError::New(env, "Expected three string arguments").ThrowAsJavaScriptException(); + return env.Undefined(); + } + + std::string path = args[0].As().Utf8Value(); + std::string key = args[1].As().Utf8Value(); + std::string confirm = args[2].As().Utf8Value(); + + Function cb; + Promise promise = CreatePromiseAndCallback(env, cb); + + auto execute_fn = [path, key, confirm](ResultAsyncWorker* worker) { + chat_ctrl ctrl = nullptr; + char* c_res = chat_migrate_init(path.c_str(), key.c_str(), confirm.c_str(), &ctrl); + worker->SetCtrl(reinterpret_cast(ctrl)); + HandleCResult(worker, c_res, "chat_migrate_init"); + }; + + ResultAsyncWorker* worker = new ResultAsyncWorker(cb, std::move(execute_fn), MigrateResultProcessor()); + worker->Queue(); + + return promise; +} + +Value ChatCloseStore(const CallbackInfo& args) { + Env env = args.Env(); + if (args.Length() < 1 || !args[0].IsBigInt()) { + TypeError::New(env, "Expected bigint (ctrl)").ThrowAsJavaScriptException(); + return env.Undefined(); + } + + chat_ctrl ctrl = FromChatCtrlBigInt(args[0]); + + Function cb; + Promise promise = CreatePromiseAndCallback(env, cb); + + auto execute_fn = [ctrl](ResultAsyncWorker* worker) { + char* c_res = chat_close_store(ctrl); + HandleCResult(worker, c_res, "chat_close_store"); + }; + + ResultAsyncWorker* worker = new ResultAsyncWorker(cb, std::move(execute_fn)); + worker->Queue(); + + return promise; +} + +Value ChatSendCmd(const CallbackInfo& args) { + Env env = args.Env(); + if (args.Length() < 2 || !args[0].IsBigInt() || !args[1].IsString()) { + TypeError::New(env, "Expected bigint (ctrl) and string (cmd)").ThrowAsJavaScriptException(); + return env.Undefined(); + } + + chat_ctrl ctrl = FromChatCtrlBigInt(args[0]); + std::string cmd = args[1].As().Utf8Value(); + + Function cb; + Promise promise = CreatePromiseAndCallback(env, cb); + + auto execute_fn = [ctrl, cmd](ResultAsyncWorker* worker) { + char* c_res = chat_send_cmd(ctrl, cmd.c_str()); + HandleCResult(worker, c_res, "chat_send_cmd"); + }; + + ResultAsyncWorker* worker = new ResultAsyncWorker(cb, std::move(execute_fn)); + worker->Queue(); + + return promise; +} + +Value ChatRecvMsgWait(const CallbackInfo& args) { + Env env = args.Env(); + if (args.Length() < 2 || !args[0].IsBigInt() || !args[1].IsNumber()) { + TypeError::New(env, "Expected bigint (ctrl), number (wait)").ThrowAsJavaScriptException(); + return env.Undefined(); + } + + chat_ctrl ctrl = FromChatCtrlBigInt(args[0]); + int wait = static_cast(args[1].As().Int32Value()); + + Function cb; + Promise promise = CreatePromiseAndCallback(env, cb); + + auto execute_fn = [ctrl, wait](ResultAsyncWorker* worker) { + char* c_res = chat_recv_msg_wait(ctrl, wait); + HandleCResult(worker, c_res, "chat_recv_msg_wait"); + }; + + ResultAsyncWorker* worker = new ResultAsyncWorker(cb, std::move(execute_fn)); + worker->Queue(); + + return promise; +} + +Value ChatWriteFile(const CallbackInfo& args) { + Env env = args.Env(); + if (args.Length() < 3 || !args[0].IsBigInt() || !args[1].IsString() || !args[2].IsArrayBuffer()) { + TypeError::New(env, "Expected bigint (ctrl), string (path), ArrayBuffer").ThrowAsJavaScriptException(); + return env.Undefined(); + } + + chat_ctrl ctrl = FromChatCtrlBigInt(args[0]); + std::string path = args[1].As().Utf8Value(); + ArrayBuffer ab = args[2].As(); + char* data = static_cast(ab.Data()); + size_t len = ab.ByteLength(); + + Function cb; + Promise promise = CreatePromiseAndCallback(env, cb); + + auto execute_fn = [ctrl, path, ab, data, len](ResultAsyncWorker* worker) { + (void)ab; // to keep ArrayBuffer alive + char* c_res = chat_write_file(ctrl, path.c_str(), data, static_cast(len)); + HandleCResult(worker, c_res, "chat_write_file"); + }; + + ResultAsyncWorker* worker = new ResultAsyncWorker(cb, std::move(execute_fn)); + worker->Queue(); + + return promise; +} + +Value ChatReadFile(const CallbackInfo& args) { + Env env = args.Env(); + if (args.Length() < 3 || !args[0].IsString() || !args[1].IsString() || !args[2].IsString()) { + TypeError::New(env, "Expected three strings (path, key, nonce)").ThrowAsJavaScriptException(); + return env.Undefined(); + } + + std::string path = args[0].As().Utf8Value(); + std::string key = args[1].As().Utf8Value(); + std::string nonce = args[2].As().Utf8Value(); + + Function cb; + Promise promise = CreatePromiseAndCallback(env, cb); + + auto execute_fn = [path, key, nonce](BinaryAsyncWorker* worker) { + char* buf = chat_read_file(path.c_str(), key.c_str(), nonce.c_str()); + if (buf == nullptr) { + worker->SetWorkerError("chat_read_file failed"); + return; + } + char status = buf[0]; + if (status == 1) { + std::string err = buf + 1; + free(buf); + worker->SetWorkerError(err); + return; + } else if (status == 0) { + uint32_t len = *(uint32_t*)(buf + 1); + worker->original_buf = buf; + worker->binary_len = len; + } else { + free(buf); + worker->SetWorkerError("Unexpected status from chat_read_file"); + return; + } + }; + + BinaryAsyncWorker* worker = new BinaryAsyncWorker(cb, std::move(execute_fn)); + worker->Queue(); + + return promise; +} + +Value ChatEncryptFile(const CallbackInfo& args) { + Env env = args.Env(); + if (args.Length() < 3 || !args[0].IsBigInt() || !args[1].IsString() || !args[2].IsString()) { + TypeError::New(env, "Expected bigint (ctrl), two strings (fromPath, toPath)").ThrowAsJavaScriptException(); + return env.Undefined(); + } + + chat_ctrl ctrl = FromChatCtrlBigInt(args[0]); + std::string fromPath = args[1].As().Utf8Value(); + std::string toPath = args[2].As().Utf8Value(); + + Function cb; + Promise promise = CreatePromiseAndCallback(env, cb); + + auto execute_fn = [ctrl, fromPath, toPath](ResultAsyncWorker* worker) { + char* c_res = chat_encrypt_file(ctrl, fromPath.c_str(), toPath.c_str()); + HandleCResult(worker, c_res, "chat_encrypt_file"); + }; + + ResultAsyncWorker* worker = new ResultAsyncWorker(cb, std::move(execute_fn)); + worker->Queue(); + + return promise; +} + +Value ChatDecryptFile(const CallbackInfo& args) { + Env env = args.Env(); + if (args.Length() < 4 || !args[0].IsString() || !args[1].IsString() || !args[2].IsString() || !args[3].IsString()) { + TypeError::New(env, "Expected four strings (fromPath, key, nonce, toPath)").ThrowAsJavaScriptException(); + return env.Undefined(); + } + + std::string fromPath = args[0].As().Utf8Value(); + std::string key = args[1].As().Utf8Value(); + std::string nonce = args[2].As().Utf8Value(); + std::string toPath = args[3].As().Utf8Value(); + + Function cb; + Promise promise = CreatePromiseAndCallback(env, cb); + + auto execute_fn = [fromPath, key, nonce, toPath](ResultAsyncWorker* worker) { + char* c_res = chat_decrypt_file(fromPath.c_str(), key.c_str(), nonce.c_str(), toPath.c_str()); + HandleCResult(worker, c_res, "chat_decrypt_file"); + }; + + ResultAsyncWorker* worker = new ResultAsyncWorker(cb, std::move(execute_fn)); + worker->Queue(); + + return promise; +} + +Object Init(Env env, Object exports) { + haskell_init(); + exports.Set("chat_migrate_init", Function::New(env, ChatMigrateInit)); + exports.Set("chat_close_store", Function::New(env, ChatCloseStore)); + exports.Set("chat_send_cmd", Function::New(env, ChatSendCmd)); + exports.Set("chat_recv_msg_wait", Function::New(env, ChatRecvMsgWait)); + exports.Set("chat_write_file", Function::New(env, ChatWriteFile)); + exports.Set("chat_read_file", Function::New(env, ChatReadFile)); + exports.Set("chat_encrypt_file", Function::New(env, ChatEncryptFile)); + exports.Set("chat_decrypt_file", Function::New(env, ChatDecryptFile)); + return exports; +} + +NODE_API_MODULE(simplex, Init) + +} diff --git a/packages/simplex-chat-nodejs/cpp/simplex.h b/packages/simplex-chat-nodejs/cpp/simplex.h new file mode 100644 index 0000000000..8e579626ed --- /dev/null +++ b/packages/simplex-chat-nodejs/cpp/simplex.h @@ -0,0 +1,46 @@ +// +// simplex.h +// SimpleX +// +// Created by Evgeny on 30/05/2022. +// Copyright © 2022 SimpleX Chat. All rights reserved. +// + +#ifndef SimpleX_h +#define SimpleX_h + +extern "C" void hs_init(int argc, char **argv[]); +extern "C" void hs_init_with_rtsopts(int * argc, char **argv[]); + +typedef long* chat_ctrl; + +// the last parameter is used to return the pointer to chat controller +extern "C" char *chat_migrate_init(const char *path, const char *key, const char *confirm, chat_ctrl *ctrl); +extern "C" char *chat_close_store(chat_ctrl ctrl); +extern "C" char *chat_reopen_store(chat_ctrl ctrl); +extern "C" char *chat_send_cmd(chat_ctrl ctrl, const char *cmd); +extern "C" char *chat_recv_msg_wait(chat_ctrl ctrl, const int wait); +extern "C" char *chat_parse_markdown(const char *str); +extern "C" char *chat_parse_server(const char *str); +extern "C" char *chat_password_hash(const char *pwd, const char *salt); +extern "C" char *chat_valid_name(const char *name); +extern "C" int chat_json_length(const char *str); +extern "C" char *chat_encrypt_media(chat_ctrl ctrl, const char *key, const char *frame, const int len); +extern "C" char *chat_decrypt_media(const char *key, const char *frame, const int len); + +// chat_write_file returns null-terminated string with JSON of WriteFileResult +extern "C" char *chat_write_file(chat_ctrl ctrl, const char *path, const char *data, const int len); + +// chat_read_file returns a buffer with: +// result status (1 byte), then if +// status == 0 (success): buffer length (uint32, 4 bytes), buffer of specified length. +// status == 1 (error): null-terminated error message string. +extern "C" char *chat_read_file(const char *path, const char *key, const char *nonce); + +// chat_encrypt_file returns null-terminated string with JSON of WriteFileResult +extern "C" char *chat_encrypt_file(chat_ctrl ctrl, const char *fromPath, const char *toPath); + +// chat_decrypt_file returns null-terminated string with the error message +extern "C" char *chat_decrypt_file(const char *fromPath, const char *key, const char *nonce, const char *toPath); + +#endif /* simplex_h */ \ No newline at end of file diff --git a/packages/simplex-chat-nodejs/docs/Namespace.api.md b/packages/simplex-chat-nodejs/docs/Namespace.api.md new file mode 100644 index 0000000000..a1b3d2ca5a --- /dev/null +++ b/packages/simplex-chat-nodejs/docs/Namespace.api.md @@ -0,0 +1,32 @@ +[**simplex-chat**](README.md) + +*** + +[simplex-chat](README.md) / api + +# api + +An API to send chat commands and receive chat events to/from chat core. +You need to use it in bot event handlers, and for any other use cases. + +## Enumerations + +- [ConnReqType](api.Enumeration.ConnReqType.md) + +## Classes + +- [ChatApi](api.Class.ChatApi.md) +- [ChatCommandError](api.Class.ChatCommandError.md) + +## Interfaces + +- [BotAddressSettings](api.Interface.BotAddressSettings.md) + +## Type Aliases + +- [EventSubscriberFunc](api.TypeAlias.EventSubscriberFunc.md) +- [EventSubscribers](api.TypeAlias.EventSubscribers.md) + +## Variables + +- [defaultBotAddressSettings](api.Variable.defaultBotAddressSettings.md) diff --git a/packages/simplex-chat-nodejs/docs/Namespace.bot.md b/packages/simplex-chat-nodejs/docs/Namespace.bot.md new file mode 100644 index 0000000000..447b0b6f68 --- /dev/null +++ b/packages/simplex-chat-nodejs/docs/Namespace.bot.md @@ -0,0 +1,20 @@ +[**simplex-chat**](README.md) + +*** + +[simplex-chat](README.md) / bot + +# bot + +A simple declarative API to run a chat-bot with a single function call. +It automates creating and updating of the bot profile, address and bot commands shown in the app UI. + +## Interfaces + +- [BotConfig](bot.Interface.BotConfig.md) +- [BotDbOpts](bot.Interface.BotDbOpts.md) +- [BotOptions](bot.Interface.BotOptions.md) + +## Functions + +- [run](bot.Function.run.md) diff --git a/packages/simplex-chat-nodejs/docs/Namespace.core.md b/packages/simplex-chat-nodejs/docs/Namespace.core.md new file mode 100644 index 0000000000..82b0d9ffba --- /dev/null +++ b/packages/simplex-chat-nodejs/docs/Namespace.core.md @@ -0,0 +1,48 @@ +[**simplex-chat**](README.md) + +*** + +[simplex-chat](README.md) / core + +# core + +A low level API to the core library - the same that is used in desktop clients. +You are unlikely to ever need to use this module directly. + +## Namespaces + +- [DBMigrationError](core.Namespace.DBMigrationError.md) +- [MigrationError](core.Namespace.MigrationError.md) +- [MTRError](core.Namespace.MTRError.md) + +## Enumerations + +- [MigrationConfirmation](core.Enumeration.MigrationConfirmation.md) + +## Classes + +- [ChatAPIError](core.Class.ChatAPIError.md) +- [ChatInitError](core.Class.ChatInitError.md) + +## Interfaces + +- [APIResult](core.Interface.APIResult.md) +- [CryptoArgs](core.Interface.CryptoArgs.md) +- [UpMigration](core.Interface.UpMigration.md) + +## Type Aliases + +- [DBMigrationError](core.TypeAlias.DBMigrationError.md) +- [MigrationError](core.TypeAlias.MigrationError.md) +- [MTRError](core.TypeAlias.MTRError.md) + +## Functions + +- [chatCloseStore](core.Function.chatCloseStore.md) +- [chatDecryptFile](core.Function.chatDecryptFile.md) +- [chatEncryptFile](core.Function.chatEncryptFile.md) +- [chatMigrateInit](core.Function.chatMigrateInit.md) +- [chatReadFile](core.Function.chatReadFile.md) +- [chatRecvMsgWait](core.Function.chatRecvMsgWait.md) +- [chatSendCmd](core.Function.chatSendCmd.md) +- [chatWriteFile](core.Function.chatWriteFile.md) diff --git a/packages/simplex-chat-nodejs/docs/Namespace.util.md b/packages/simplex-chat-nodejs/docs/Namespace.util.md new file mode 100644 index 0000000000..a6e7d9c7f3 --- /dev/null +++ b/packages/simplex-chat-nodejs/docs/Namespace.util.md @@ -0,0 +1,25 @@ +[**simplex-chat**](README.md) + +*** + +[simplex-chat](README.md) / util + +# util + +Useful functions for chat events and types. + +## Interfaces + +- [BotCommand](util.Interface.BotCommand.md) + +## Functions + +- [botAddressSettings](util.Function.botAddressSettings.md) +- [chatInfoName](util.Function.chatInfoName.md) +- [chatInfoRef](util.Function.chatInfoRef.md) +- [ciBotCommand](util.Function.ciBotCommand.md) +- [ciContentText](util.Function.ciContentText.md) +- [contactAddressStr](util.Function.contactAddressStr.md) +- [fromLocalProfile](util.Function.fromLocalProfile.md) +- [reactionText](util.Function.reactionText.md) +- [senderName](util.Function.senderName.md) diff --git a/packages/simplex-chat-nodejs/docs/README.md b/packages/simplex-chat-nodejs/docs/README.md new file mode 100644 index 0000000000..f954730ed6 --- /dev/null +++ b/packages/simplex-chat-nodejs/docs/README.md @@ -0,0 +1,12 @@ +**simplex-chat** + +*** + +# simplex-chat + +## Namespaces + +- [api](Namespace.api.md) +- [bot](Namespace.bot.md) +- [core](Namespace.core.md) +- [util](Namespace.util.md) diff --git a/packages/simplex-chat-nodejs/docs/api.Class.ChatApi.md b/packages/simplex-chat-nodejs/docs/api.Class.ChatApi.md new file mode 100644 index 0000000000..6812740aa2 --- /dev/null +++ b/packages/simplex-chat-nodejs/docs/api.Class.ChatApi.md @@ -0,0 +1,1602 @@ +[**simplex-chat**](README.md) + +*** + +[simplex-chat](README.md) / [api](Namespace.api.md) / ChatApi + +# Class: ChatApi + +Defined in: [src/api.ts:62](../src/api.ts#L62) + +Main API class for interacting with the chat core library. + +## Properties + +### ctrl\_ + +> `protected` **ctrl\_**: `bigint` \| `undefined` + +Defined in: [src/api.ts:68](../src/api.ts#L68) + +## Accessors + +### ctrl + +#### Get Signature + +> **get** **ctrl**(): `bigint` + +Defined in: [src/api.ts:295](../src/api.ts#L295) + +Chat controller reference + +##### Returns + +`bigint` + +*** + +### initialized + +#### Get Signature + +> **get** **initialized**(): `boolean` + +Defined in: [src/api.ts:281](../src/api.ts#L281) + +Chat controller is initialized + +##### Returns + +`boolean` + +*** + +### started + +#### Get Signature + +> **get** **started**(): `boolean` + +Defined in: [src/api.ts:288](../src/api.ts#L288) + +Chat controller is started + +##### Returns + +`boolean` + +## Methods + +### apiAcceptContactRequest() + +> **apiAcceptContactRequest**(`contactReqId`): `Promise`\<`Contact`\> + +Defined in: [src/api.ts:697](../src/api.ts#L697) + +Accept contact request. +Network usage: interactive. + +#### Parameters + +##### contactReqId + +`number` + +#### Returns + +`Promise`\<`Contact`\> + +*** + +### apiAcceptMember() + +> **apiAcceptMember**(`groupId`, `groupMemberId`, `memberRole`): `Promise`\<`GroupMember`\> + +Defined in: [src/api.ts:517](../src/api.ts#L517) + +Accept group member. Requires Admin role. +Network usage: background. + +#### Parameters + +##### groupId + +`number` + +##### groupMemberId + +`number` + +##### memberRole + +`GroupMemberRole` + +#### Returns + +`Promise`\<`GroupMember`\> + +*** + +### apiAddMember() + +> **apiAddMember**(`groupId`, `contactId`, `memberRole`): `Promise`\<`GroupMember`\> + +Defined in: [src/api.ts:497](../src/api.ts#L497) + +Add contact to group. Requires bot to have Admin role. +Network usage: interactive. + +#### Parameters + +##### groupId + +`number` + +##### contactId + +`number` + +##### memberRole + +`GroupMemberRole` + +#### Returns + +`Promise`\<`GroupMember`\> + +*** + +### apiBlockMembersForAll() + +> **apiBlockMembersForAll**(`groupId`, `groupMemberIds`, `blocked`): `Promise`\<`void`\> + +Defined in: [src/api.ts:537](../src/api.ts#L537) + +Block members. Requires Moderator role. +Network usage: background. + +#### Parameters + +##### groupId + +`number` + +##### groupMemberIds + +`number`[] + +##### blocked + +`boolean` + +#### Returns + +`Promise`\<`void`\> + +*** + +### apiCancelFile() + +> **apiCancelFile**(`fileId`): `Promise`\<`void`\> + +Defined in: [src/api.ts:487](../src/api.ts#L487) + +Cancel file. +Network usage: background. + +#### Parameters + +##### fileId + +`number` + +#### Returns + +`Promise`\<`void`\> + +*** + +### apiChatItemReaction() + +> **apiChatItemReaction**(`chatType`, `chatId`, `chatItemId`, `add`, `reaction`): `Promise`\<`ChatItemDeletion`[]\> + +Defined in: [src/api.ts:461](../src/api.ts#L461) + +Add/remove message reaction. +Network usage: background. + +#### Parameters + +##### chatType + +`ChatType` + +##### chatId + +`number` + +##### chatItemId + +`number` + +##### add + +`boolean` + +##### reaction + +`MsgReaction` + +#### Returns + +`Promise`\<`ChatItemDeletion`[]\> + +*** + +### apiConnect() + +> **apiConnect**(`userId`, `incognito`, `preparedLink?`): `Promise`\<[`ConnReqType`](api.Enumeration.ConnReqType.md)\> + +Defined in: [src/api.ts:666](../src/api.ts#L666) + +Connect via prepared SimpleX link. The link can be 1-time invitation link, contact address or group link +Network usage: interactive. + +#### Parameters + +##### userId + +`number` + +##### incognito + +`boolean` + +##### preparedLink? + +`CreatedConnLink` + +#### Returns + +`Promise`\<[`ConnReqType`](api.Enumeration.ConnReqType.md)\> + +*** + +### apiConnectActiveUser() + +> **apiConnectActiveUser**(`connLink`): `Promise`\<[`ConnReqType`](api.Enumeration.ConnReqType.md)\> + +Defined in: [src/api.ts:675](../src/api.ts#L675) + +Connect via SimpleX link as string in the active user profile. +Network usage: interactive. + +#### Parameters + +##### connLink + +`string` + +#### Returns + +`Promise`\<[`ConnReqType`](api.Enumeration.ConnReqType.md)\> + +*** + +### apiConnectPlan() + +> **apiConnectPlan**(`userId`, `connectionLink`): `Promise`\<\[`ConnectionPlan`, `CreatedConnLink`\]\> + +Defined in: [src/api.ts:656](../src/api.ts#L656) + +Determine SimpleX link type and if the bot is already connected via this link. +Network usage: interactive. + +#### Parameters + +##### userId + +`number` + +##### connectionLink + +`string` + +#### Returns + +`Promise`\<\[`ConnectionPlan`, `CreatedConnLink`\]\> + +*** + +### apiCreateActiveUser() + +> **apiCreateActiveUser**(`profile?`): `Promise`\<`User`\> + +Defined in: [src/api.ts:774](../src/api.ts#L774) + +Create new user profile +Network usage: no. + +#### Parameters + +##### profile? + +`Profile` + +#### Returns + +`Promise`\<`User`\> + +*** + +### apiCreateGroupLink() + +> **apiCreateGroupLink**(`groupId`, `memberRole`): `Promise`\<`string`\> + +Defined in: [src/api.ts:597](../src/api.ts#L597) + +Create group link. +Network usage: interactive. + +#### Parameters + +##### groupId + +`number` + +##### memberRole + +`GroupMemberRole` + +#### Returns + +`Promise`\<`string`\> + +*** + +### apiCreateLink() + +> **apiCreateLink**(`userId`): `Promise`\<`string`\> + +Defined in: [src/api.ts:643](../src/api.ts#L643) + +Create 1-time invitation link. +Network usage: interactive. + +#### Parameters + +##### userId + +`number` + +#### Returns + +`Promise`\<`string`\> + +*** + +### apiCreateUserAddress() + +> **apiCreateUserAddress**(`userId`): `Promise`\<`CreatedConnLink`\> + +Defined in: [src/api.ts:312](../src/api.ts#L312) + +Create bot address. +Network usage: interactive. + +#### Parameters + +##### userId + +`number` + +#### Returns + +`Promise`\<`CreatedConnLink`\> + +*** + +### apiDeleteChat() + +> **apiDeleteChat**(`chatType`, `chatId`, `deleteMode`): `Promise`\<`void`\> + +Defined in: [src/api.ts:737](../src/api.ts#L737) + +Delete chat. +Network usage: background. + +#### Parameters + +##### chatType + +`ChatType` + +##### chatId + +`number` + +##### deleteMode + +`ChatDeleteMode` = `...` + +#### Returns + +`Promise`\<`void`\> + +*** + +### apiDeleteChatItems() + +> **apiDeleteChatItems**(`chatType`, `chatId`, `chatItemIds`, `deleteMode`): `Promise`\<`ChatItemDeletion`[]\> + +Defined in: [src/api.ts:436](../src/api.ts#L436) + +Delete message. +Network usage: background. + +#### Parameters + +##### chatType + +`ChatType` + +##### chatId + +`number` + +##### chatItemIds + +`number`[] + +##### deleteMode + +`CIDeleteMode` + +#### Returns + +`Promise`\<`ChatItemDeletion`[]\> + +*** + +### apiDeleteGroupLink() + +> **apiDeleteGroupLink**(`groupId`): `Promise`\<`void`\> + +Defined in: [src/api.ts:619](../src/api.ts#L619) + +Delete group link. +Network usage: background. + +#### Parameters + +##### groupId + +`number` + +#### Returns + +`Promise`\<`void`\> + +*** + +### apiDeleteMemberChatItem() + +> **apiDeleteMemberChatItem**(`groupId`, `chatItemIds`): `Promise`\<`ChatItemDeletion`[]\> + +Defined in: [src/api.ts:451](../src/api.ts#L451) + +Moderate message. Requires Moderator role (and higher than message author's). +Network usage: background. + +#### Parameters + +##### groupId + +`number` + +##### chatItemIds + +`number`[] + +#### Returns + +`Promise`\<`ChatItemDeletion`[]\> + +*** + +### apiDeleteUser() + +> **apiDeleteUser**(`userId`, `delSMPQueues`, `viewPwd?`): `Promise`\<`void`\> + +Defined in: [src/api.ts:804](../src/api.ts#L804) + +Delete user profile. +Network usage: background. + +#### Parameters + +##### userId + +`number` + +##### delSMPQueues + +`boolean` + +##### viewPwd? + +`string` + +#### Returns + +`Promise`\<`void`\> + +*** + +### apiDeleteUserAddress() + +> **apiDeleteUserAddress**(`userId`): `Promise`\<`void`\> + +Defined in: [src/api.ts:322](../src/api.ts#L322) + +Deletes a user address. +Network usage: background. + +#### Parameters + +##### userId + +`number` + +#### Returns + +`Promise`\<`void`\> + +*** + +### apiGetActiveUser() + +> **apiGetActiveUser**(): `Promise`\<`User` \| `undefined`\> + +Defined in: [src/api.ts:754](../src/api.ts#L754) + +Get active user profile +Network usage: no. + +#### Returns + +`Promise`\<`User` \| `undefined`\> + +*** + +### apiGetGroupLink() + +> **apiGetGroupLink**(`groupId`): `Promise`\<`GroupLink`\> + +Defined in: [src/api.ts:628](../src/api.ts#L628) + +Get group link. +Network usage: no. + +#### Parameters + +##### groupId + +`number` + +#### Returns + +`Promise`\<`GroupLink`\> + +*** + +### apiGetGroupLinkStr() + +> **apiGetGroupLinkStr**(`groupId`): `Promise`\<`string`\> + +Defined in: [src/api.ts:634](../src/api.ts#L634) + +#### Parameters + +##### groupId + +`number` + +#### Returns + +`Promise`\<`string`\> + +*** + +### apiGetUserAddress() + +> **apiGetUserAddress**(`userId`): `Promise`\<`UserContactLink` \| `undefined`\> + +Defined in: [src/api.ts:332](../src/api.ts#L332) + +Get bot address and settings. +Network usage: no. + +#### Parameters + +##### userId + +`number` + +#### Returns + +`Promise`\<`UserContactLink` \| `undefined`\> + +*** + +### apiJoinGroup() + +> **apiJoinGroup**(`groupId`): `Promise`\<`GroupInfo`\> + +Defined in: [src/api.ts:507](../src/api.ts#L507) + +Join group. +Network usage: interactive. + +#### Parameters + +##### groupId + +`number` + +#### Returns + +`Promise`\<`GroupInfo`\> + +*** + +### apiLeaveGroup() + +> **apiLeaveGroup**(`groupId`): `Promise`\<`GroupInfo`\> + +Defined in: [src/api.ts:557](../src/api.ts#L557) + +Leave group. +Network usage: background. + +#### Parameters + +##### groupId + +`number` + +#### Returns + +`Promise`\<`GroupInfo`\> + +*** + +### apiListContacts() + +> **apiListContacts**(`userId`): `Promise`\<`Contact`[]\> + +Defined in: [src/api.ts:717](../src/api.ts#L717) + +Get contacts. +Network usage: no. + +#### Parameters + +##### userId + +`number` + +#### Returns + +`Promise`\<`Contact`[]\> + +*** + +### apiListGroups() + +> **apiListGroups**(`userId`, `contactId?`, `search?`): `Promise`\<`GroupInfo`[]\> + +Defined in: [src/api.ts:727](../src/api.ts#L727) + +Get groups. +Network usage: no. + +#### Parameters + +##### userId + +`number` + +##### contactId? + +`number` + +##### search? + +`string` + +#### Returns + +`Promise`\<`GroupInfo`[]\> + +*** + +### apiListMembers() + +> **apiListMembers**(`groupId`): `Promise`\<`GroupMember`[]\> + +Defined in: [src/api.ts:567](../src/api.ts#L567) + +Get group members. +Network usage: no. + +#### Parameters + +##### groupId + +`number` + +#### Returns + +`Promise`\<`GroupMember`[]\> + +*** + +### apiListUsers() + +> **apiListUsers**(): `Promise`\<`UserInfo`[]\> + +Defined in: [src/api.ts:784](../src/api.ts#L784) + +Get all user profiles +Network usage: no. + +#### Returns + +`Promise`\<`UserInfo`[]\> + +*** + +### apiNewGroup() + +> **apiNewGroup**(`userId`, `groupProfile`): `Promise`\<`GroupInfo`\> + +Defined in: [src/api.ts:577](../src/api.ts#L577) + +Create group. +Network usage: no. + +#### Parameters + +##### userId + +`number` + +##### groupProfile + +`GroupProfile` + +#### Returns + +`Promise`\<`GroupInfo`\> + +*** + +### apiReceiveFile() + +> **apiReceiveFile**(`fileId`): `Promise`\<`AChatItem`\> + +Defined in: [src/api.ts:477](../src/api.ts#L477) + +Receive file. +Network usage: no. + +#### Parameters + +##### fileId + +`number` + +#### Returns + +`Promise`\<`AChatItem`\> + +*** + +### apiRejectContactRequest() + +> **apiRejectContactRequest**(`contactReqId`): `Promise`\<`void`\> + +Defined in: [src/api.ts:707](../src/api.ts#L707) + +Reject contact request. The user who sent the request is **not notified**. +Network usage: no. + +#### Parameters + +##### contactReqId + +`number` + +#### Returns + +`Promise`\<`void`\> + +*** + +### apiRemoveMembers() + +> **apiRemoveMembers**(`groupId`, `memberIds`, `withMessages`): `Promise`\<`GroupMember`[]\> + +Defined in: [src/api.ts:547](../src/api.ts#L547) + +Remove members. Requires Admin role. +Network usage: background. + +#### Parameters + +##### groupId + +`number` + +##### memberIds + +`number`[] + +##### withMessages + +`boolean` = `false` + +#### Returns + +`Promise`\<`GroupMember`[]\> + +*** + +### apiSendMessages() + +> **apiSendMessages**(`chat`, `messages`, `liveMessage`): `Promise`\<`AChatItem`[]\> + +Defined in: [src/api.ts:381](../src/api.ts#L381) + +Send messages. +Network usage: background. + +#### Parameters + +##### chat + +`ChatInfo` | `ChatRef` | \[`ChatType`, `number`\] + +##### messages + +`ComposedMessage`[] + +##### liveMessage + +`boolean` = `false` + +#### Returns + +`Promise`\<`AChatItem`[]\> + +*** + +### apiSendTextMessage() + +> **apiSendTextMessage**(`chat`, `text`, `inReplyTo?`): `Promise`\<`AChatItem`[]\> + +Defined in: [src/api.ts:403](../src/api.ts#L403) + +Send text message. +Network usage: background. + +#### Parameters + +##### chat + +`ChatInfo` | `ChatRef` | \[`ChatType`, `number`\] + +##### text + +`string` + +##### inReplyTo? + +`number` + +#### Returns + +`Promise`\<`AChatItem`[]\> + +*** + +### apiSendTextReply() + +> **apiSendTextReply**(`chatItem`, `text`): `Promise`\<`AChatItem`[]\> + +Defined in: [src/api.ts:411](../src/api.ts#L411) + +Send text message in reply to received message. +Network usage: background. + +#### Parameters + +##### chatItem + +`AChatItem` + +##### text + +`string` + +#### Returns + +`Promise`\<`AChatItem`[]\> + +*** + +### apiSetActiveUser() + +> **apiSetActiveUser**(`userId`, `viewPwd?`): `Promise`\<`User`\> + +Defined in: [src/api.ts:794](../src/api.ts#L794) + +Set active user profile +Network usage: no. + +#### Parameters + +##### userId + +`number` + +##### viewPwd? + +`string` + +#### Returns + +`Promise`\<`User`\> + +*** + +### apiSetAddressSettings() + +> **apiSetAddressSettings**(`userId`, `__namedParameters`): `Promise`\<`void`\> + +Defined in: [src/api.ts:364](../src/api.ts#L364) + +Set bot address settings. +Network usage: interactive. + +#### Parameters + +##### userId + +`number` + +##### \_\_namedParameters + +[`BotAddressSettings`](api.Interface.BotAddressSettings.md) + +#### Returns + +`Promise`\<`void`\> + +*** + +### apiSetContactPrefs() + +> **apiSetContactPrefs**(`contactId`, `preferences`): `Promise`\<`void`\> + +Defined in: [src/api.ts:830](../src/api.ts#L830) + +Configure chat preference overrides for the contact. +Network usage: background. + +#### Parameters + +##### contactId + +`number` + +##### preferences + +`Preferences` + +#### Returns + +`Promise`\<`void`\> + +*** + +### apiSetGroupLinkMemberRole() + +> **apiSetGroupLinkMemberRole**(`groupId`, `memberRole`): `Promise`\<`void`\> + +Defined in: [src/api.ts:610](../src/api.ts#L610) + +Set member role for group link. +Network usage: no. + +#### Parameters + +##### groupId + +`number` + +##### memberRole + +`GroupMemberRole` + +#### Returns + +`Promise`\<`void`\> + +*** + +### apiSetMembersRole() + +> **apiSetMembersRole**(`groupId`, `groupMemberIds`, `memberRole`): `Promise`\<`void`\> + +Defined in: [src/api.ts:527](../src/api.ts#L527) + +Set members role. Requires Admin role. +Network usage: background. + +#### Parameters + +##### groupId + +`number` + +##### groupMemberIds + +`number`[] + +##### memberRole + +`GroupMemberRole` + +#### Returns + +`Promise`\<`void`\> + +*** + +### apiSetProfileAddress() + +> **apiSetProfileAddress**(`userId`, `enable`): `Promise`\<`UserProfileUpdateSummary`\> + +Defined in: [src/api.ts:350](../src/api.ts#L350) + +Add address to bot profile. +Network usage: interactive. + +#### Parameters + +##### userId + +`number` + +##### enable + +`boolean` + +#### Returns + +`Promise`\<`UserProfileUpdateSummary`\> + +*** + +### apiUpdateChatItem() + +> **apiUpdateChatItem**(`chatType`, `chatId`, `chatItemId`, `msgContent`, `liveMessage`): `Promise`\<`ChatItem`\> + +Defined in: [src/api.ts:419](../src/api.ts#L419) + +Update message. +Network usage: background. + +#### Parameters + +##### chatType + +`ChatType` + +##### chatId + +`number` + +##### chatItemId + +`number` + +##### msgContent + +`MsgContent` + +##### liveMessage + +`false` + +#### Returns + +`Promise`\<`ChatItem`\> + +*** + +### apiUpdateGroupProfile() + +> **apiUpdateGroupProfile**(`groupId`, `groupProfile`): `Promise`\<`GroupInfo`\> + +Defined in: [src/api.ts:587](../src/api.ts#L587) + +Update group profile. +Network usage: background. + +#### Parameters + +##### groupId + +`number` + +##### groupProfile + +`GroupProfile` + +#### Returns + +`Promise`\<`GroupInfo`\> + +*** + +### apiUpdateProfile() + +> **apiUpdateProfile**(`userId`, `profile`): `Promise`\<`UserProfileUpdateSummary` \| `undefined`\> + +Defined in: [src/api.ts:814](../src/api.ts#L814) + +Update user profile. +Network usage: background. + +#### Parameters + +##### userId + +`number` + +##### profile + +`Profile` + +#### Returns + +`Promise`\<`UserProfileUpdateSummary` \| `undefined`\> + +*** + +### close() + +> **close**(): `Promise`\<`void`\> + +Defined in: [src/api.ts:114](../src/api.ts#L114) + +Close chat database. +Usually doesn't need to be called in chat bots. + +#### Returns + +`Promise`\<`void`\> + +*** + +### off() + +> **off**\<`K`\>(`event`, `subscriber`): `void` + +Defined in: [src/api.ts:253](../src/api.ts#L253) + +Unsubscribe all or a specific handler from a specific event. + +#### Type Parameters + +##### K + +`K` *extends* `Tag` + +#### Parameters + +##### event + +`K` + +The event type to unsubscribe from. + +##### subscriber + +An optional subscriber function for the event. + +[`EventSubscriberFunc`](api.TypeAlias.EventSubscriberFunc.md)\<`K`\> | `undefined` + +#### Returns + +`void` + +*** + +### offAny() + +> **offAny**(`receiver`): `void` + +Defined in: [src/api.ts:269](../src/api.ts#L269) + +Unsubscribe all or a specific handler from any events. + +#### Parameters + +##### receiver + +An optional subscriber function for the event. + +[`EventSubscriberFunc`](api.TypeAlias.EventSubscriberFunc.md)\<`Tag`\> | `undefined` + +#### Returns + +`void` + +*** + +### on() + +#### Call Signature + +> **on**\<`K`\>(`subscribers`): `void` + +Defined in: [src/api.ts:163](../src/api.ts#L163) + +Subscribe multiple event handlers at once. + +##### Type Parameters + +###### K + +`K` *extends* `Tag` + +##### Parameters + +###### subscribers + +[`EventSubscribers`](api.TypeAlias.EventSubscribers.md) + +An object mapping event types (CEvt.Tag) to their subscriber functions. + +##### Returns + +`void` + +##### Throws + +If the same function is subscribed to event. + +#### Call Signature + +> **on**\<`K`\>(`event`, `subscriber`): `void` + +Defined in: [src/api.ts:171](../src/api.ts#L171) + +Subscribe a handler to a specific event. + +##### Type Parameters + +###### K + +`K` *extends* `Tag` + +##### Parameters + +###### event + +`K` + +The event type to subscribe to. + +###### subscriber + +[`EventSubscriberFunc`](api.TypeAlias.EventSubscriberFunc.md)\<`K`\> + +The subscriber function for the event. + +##### Returns + +`void` + +##### Throws + +If the same function is subscribed to event. + +*** + +### onAny() + +> **onAny**(`receiver`): `void` + +Defined in: [src/api.ts:194](../src/api.ts#L194) + +Subscribe a handler to any event. + +#### Parameters + +##### receiver + +[`EventSubscriberFunc`](api.TypeAlias.EventSubscriberFunc.md)\<`Tag`\> + +The receiver function for any event. + +#### Returns + +`void` + +#### Throws + +If the same function is subscribed to event. + +*** + +### once() + +> **once**\<`K`\>(`event`, `subscriber`): `void` + +Defined in: [src/api.ts:205](../src/api.ts#L205) + +Subscribe a handler to a specific event to be delivered one time. + +#### Type Parameters + +##### K + +`K` *extends* `Tag` + +#### Parameters + +##### event + +`K` + +The event type to subscribe to. + +##### subscriber + +[`EventSubscriberFunc`](api.TypeAlias.EventSubscriberFunc.md)\<`K`\> + +The subscriber function for the event. + +#### Returns + +`void` + +#### Throws + +If the same function is subscribed to event. + +*** + +### recvChatEvent() + +> **recvChatEvent**(`wait`): `Promise`\<`ChatEvent` \| `undefined`\> + +Defined in: [src/api.ts:304](../src/api.ts#L304) + +#### Parameters + +##### wait + +`number` = `5_000_000` + +#### Returns + +`Promise`\<`ChatEvent` \| `undefined`\> + +*** + +### sendChatCmd() + +> **sendChatCmd**(`cmd`): `Promise`\<`ChatResponse`\> + +Defined in: [src/api.ts:300](../src/api.ts#L300) + +#### Parameters + +##### cmd + +`string` + +#### Returns + +`Promise`\<`ChatResponse`\> + +*** + +### startChat() + +> **startChat**(): `Promise`\<`void`\> + +Defined in: [src/api.ts:88](../src/api.ts#L88) + +Start chat controller. Must be called with the existing user profile. + +#### Returns + +`Promise`\<`void`\> + +*** + +### stopChat() + +> **stopChat**(): `Promise`\<`void`\> + +Defined in: [src/api.ts:102](../src/api.ts#L102) + +Stop chat controller. +Must be called before closing the database. +Usually doesn't need to be called in chat bots. + +#### Returns + +`Promise`\<`void`\> + +*** + +### wait() + +#### Call Signature + +> **wait**\<`K`\>(`event`): `Promise`\<`ChatEvent` & \{ `type`: `K`; \}\> + +Defined in: [src/api.ts:213](../src/api.ts#L213) + +Waits for specific event, with an optional predicate. +Returns `undefined` on timeout if specified. + +##### Type Parameters + +###### K + +`K` *extends* `Tag` + +##### Parameters + +###### event + +`K` + +##### Returns + +`Promise`\<`ChatEvent` & \{ `type`: `K`; \}\> + +#### Call Signature + +> **wait**\<`K`\>(`event`, `predicate`): `Promise`\<`ChatEvent` & \{ `type`: `K`; \}\> + +Defined in: [src/api.ts:214](../src/api.ts#L214) + +Waits for specific event, with an optional predicate. +Returns `undefined` on timeout if specified. + +##### Type Parameters + +###### K + +`K` *extends* `Tag` + +##### Parameters + +###### event + +`K` + +###### predicate + +(`event`) => `boolean` | `undefined` + +##### Returns + +`Promise`\<`ChatEvent` & \{ `type`: `K`; \}\> + +#### Call Signature + +> **wait**\<`K`\>(`event`, `timeout`): `Promise`\ + +Defined in: [src/api.ts:215](../src/api.ts#L215) + +Waits for specific event, with an optional predicate. +Returns `undefined` on timeout if specified. + +##### Type Parameters + +###### K + +`K` *extends* `Tag` + +##### Parameters + +###### event + +`K` + +###### timeout + +`number` + +##### Returns + +`Promise`\ + +#### Call Signature + +> **wait**\<`K`\>(`event`, `predicate`, `timeout`): `Promise`\ + +Defined in: [src/api.ts:216](../src/api.ts#L216) + +Waits for specific event, with an optional predicate. +Returns `undefined` on timeout if specified. + +##### Type Parameters + +###### K + +`K` *extends* `Tag` + +##### Parameters + +###### event + +`K` + +###### predicate + +(`event`) => `boolean` | `undefined` + +###### timeout + +`number` + +##### Returns + +`Promise`\ + +*** + +### init() + +> `static` **init**(`dbFilePrefix`, `dbKey?`, `confirm?`): `Promise`\<`ChatApi`\> + +Defined in: [src/api.ts:76](../src/api.ts#L76) + +Initializes the ChatApi. + +#### Parameters + +##### dbFilePrefix + +`string` + +File prefix for the database files. + +##### dbKey? + +`string` = `""` + +Database encryption key. + +##### confirm? + +[`MigrationConfirmation`](core.Enumeration.MigrationConfirmation.md) = `core.MigrationConfirmation.YesUp` + +Migration confirmation mode. + +#### Returns + +`Promise`\<`ChatApi`\> diff --git a/packages/simplex-chat-nodejs/docs/api.Class.ChatCommandError.md b/packages/simplex-chat-nodejs/docs/api.Class.ChatCommandError.md new file mode 100644 index 0000000000..a4955cb3d9 --- /dev/null +++ b/packages/simplex-chat-nodejs/docs/api.Class.ChatCommandError.md @@ -0,0 +1,205 @@ +[**simplex-chat**](README.md) + +*** + +[simplex-chat](README.md) / [api](Namespace.api.md) / ChatCommandError + +# Class: ChatCommandError + +Defined in: [src/api.ts:5](../src/api.ts#L5) + +## Extends + +- `Error` + +## Constructors + +### Constructor + +> **new ChatCommandError**(`message`, `response`): `ChatCommandError` + +Defined in: [src/api.ts:6](../src/api.ts#L6) + +#### Parameters + +##### message + +`string` + +##### response + +`ChatResponse` + +#### Returns + +`ChatCommandError` + +#### Overrides + +`Error.constructor` + +## Properties + +### message + +> **message**: `string` + +Defined in: [src/api.ts:6](../src/api.ts#L6) + +#### Inherited from + +`Error.message` + +*** + +### name + +> **name**: `string` + +Defined in: [node\_modules/typescript/lib/lib.es5.d.ts:1076](../node_modules/typescript/lib/lib.es5.d.ts#L1076) + +#### Inherited from + +`Error.name` + +*** + +### response + +> **response**: `ChatResponse` + +Defined in: [src/api.ts:6](../src/api.ts#L6) + +*** + +### stack? + +> `optional` **stack**: `string` + +Defined in: [node\_modules/typescript/lib/lib.es5.d.ts:1078](../node_modules/typescript/lib/lib.es5.d.ts#L1078) + +#### Inherited from + +`Error.stack` + +*** + +### stackTraceLimit + +> `static` **stackTraceLimit**: `number` + +Defined in: [node\_modules/@types/node/globals.d.ts:67](../node_modules/@types/node/globals.d.ts#L67) + +The `Error.stackTraceLimit` property specifies the number of stack frames +collected by a stack trace (whether generated by `new Error().stack` or +`Error.captureStackTrace(obj)`). + +The default value is `10` but may be set to any valid JavaScript number. Changes +will affect any stack trace captured _after_ the value has been changed. + +If set to a non-number value, or set to a negative number, stack traces will +not capture any frames. + +#### Inherited from + +`Error.stackTraceLimit` + +## Methods + +### captureStackTrace() + +> `static` **captureStackTrace**(`targetObject`, `constructorOpt?`): `void` + +Defined in: [node\_modules/@types/node/globals.d.ts:51](../node_modules/@types/node/globals.d.ts#L51) + +Creates a `.stack` property on `targetObject`, which when accessed returns +a string representing the location in the code at which +`Error.captureStackTrace()` was called. + +```js +const myObject = {}; +Error.captureStackTrace(myObject); +myObject.stack; // Similar to `new Error().stack` +``` + +The first line of the trace will be prefixed with +`${myObject.name}: ${myObject.message}`. + +The optional `constructorOpt` argument accepts a function. If given, all frames +above `constructorOpt`, including `constructorOpt`, will be omitted from the +generated stack trace. + +The `constructorOpt` argument is useful for hiding implementation +details of error generation from the user. For instance: + +```js +function a() { + b(); +} + +function b() { + c(); +} + +function c() { + // Create an error without stack trace to avoid calculating the stack trace twice. + const { stackTraceLimit } = Error; + Error.stackTraceLimit = 0; + const error = new Error(); + Error.stackTraceLimit = stackTraceLimit; + + // Capture the stack trace above function b + Error.captureStackTrace(error, b); // Neither function c, nor b is included in the stack trace + throw error; +} + +a(); +``` + +#### Parameters + +##### targetObject + +`object` + +##### constructorOpt? + +`Function` + +#### Returns + +`void` + +#### Inherited from + +`Error.captureStackTrace` + +*** + +### prepareStackTrace() + +> `static` **prepareStackTrace**(`err`, `stackTraces`): `any` + +Defined in: [node\_modules/@types/node/globals.d.ts:55](../node_modules/@types/node/globals.d.ts#L55) + +#### Parameters + +##### err + +`Error` + +##### stackTraces + +`CallSite`[] + +#### Returns + +`any` + +#### See + +https://v8.dev/docs/stack-trace-api#customizing-stack-traces + +#### Inherited from + +`Error.prepareStackTrace` diff --git a/packages/simplex-chat-nodejs/docs/api.Enumeration.ConnReqType.md b/packages/simplex-chat-nodejs/docs/api.Enumeration.ConnReqType.md new file mode 100644 index 0000000000..dcabb85b67 --- /dev/null +++ b/packages/simplex-chat-nodejs/docs/api.Enumeration.ConnReqType.md @@ -0,0 +1,27 @@ +[**simplex-chat**](README.md) + +*** + +[simplex-chat](README.md) / [api](Namespace.api.md) / ConnReqType + +# Enumeration: ConnReqType + +Defined in: [src/api.ts:15](../src/api.ts#L15) + +Connection request types. + +## Enumeration Members + +### Contact + +> **Contact**: `"contact"` + +Defined in: [src/api.ts:17](../src/api.ts#L17) + +*** + +### Invitation + +> **Invitation**: `"invitation"` + +Defined in: [src/api.ts:16](../src/api.ts#L16) diff --git a/packages/simplex-chat-nodejs/docs/api.Interface.BotAddressSettings.md b/packages/simplex-chat-nodejs/docs/api.Interface.BotAddressSettings.md new file mode 100644 index 0000000000..efb4a75e81 --- /dev/null +++ b/packages/simplex-chat-nodejs/docs/api.Interface.BotAddressSettings.md @@ -0,0 +1,60 @@ +[**simplex-chat**](README.md) + +*** + +[simplex-chat](README.md) / [api](Namespace.api.md) / BotAddressSettings + +# Interface: BotAddressSettings + +Defined in: [src/api.ts:23](../src/api.ts#L23) + +Bot address settings. + +## Properties + +### autoAccept? + +> `optional` **autoAccept**: `boolean` + +Defined in: [src/api.ts:28](../src/api.ts#L28) + +Automatically accept contact requests. + +#### Default + +```ts +true +``` + +*** + +### businessAddress? + +> `optional` **businessAddress**: `boolean` + +Defined in: [src/api.ts:41](../src/api.ts#L41) + +Business contact address. +For all requests business chats will be created where other participants can be added. + +#### Default + +```ts +false +``` + +*** + +### welcomeMessage? + +> `optional` **welcomeMessage**: `string` \| `MsgContent` + +Defined in: [src/api.ts:34](../src/api.ts#L34) + +Optional welcome message to show before connection to the users. + +#### Default + +```ts +undefined (no welcome message) +``` diff --git a/packages/simplex-chat-nodejs/docs/api.TypeAlias.EventSubscriberFunc.md b/packages/simplex-chat-nodejs/docs/api.TypeAlias.EventSubscriberFunc.md new file mode 100644 index 0000000000..6197befc8a --- /dev/null +++ b/packages/simplex-chat-nodejs/docs/api.TypeAlias.EventSubscriberFunc.md @@ -0,0 +1,27 @@ +[**simplex-chat**](README.md) + +*** + +[simplex-chat](README.md) / [api](Namespace.api.md) / EventSubscriberFunc + +# Type Alias: EventSubscriberFunc()\ + +> **EventSubscriberFunc**\<`K`\> = (`event`) => `void` \| `Promise`\<`void`\> + +Defined in: [src/api.ts:50](../src/api.ts#L50) + +## Type Parameters + +### K + +`K` *extends* `CEvt.Tag` + +## Parameters + +### event + +`ChatEvent` & \{ `type`: `K`; \} + +## Returns + +`void` \| `Promise`\<`void`\> diff --git a/packages/simplex-chat-nodejs/docs/api.TypeAlias.EventSubscribers.md b/packages/simplex-chat-nodejs/docs/api.TypeAlias.EventSubscribers.md new file mode 100644 index 0000000000..3b63d11bd4 --- /dev/null +++ b/packages/simplex-chat-nodejs/docs/api.TypeAlias.EventSubscribers.md @@ -0,0 +1,11 @@ +[**simplex-chat**](README.md) + +*** + +[simplex-chat](README.md) / [api](Namespace.api.md) / EventSubscribers + +# Type Alias: EventSubscribers + +> **EventSubscribers** = `{ [K in CEvt.Tag]?: EventSubscriberFunc }` + +Defined in: [src/api.ts:52](../src/api.ts#L52) diff --git a/packages/simplex-chat-nodejs/docs/api.Variable.defaultBotAddressSettings.md b/packages/simplex-chat-nodejs/docs/api.Variable.defaultBotAddressSettings.md new file mode 100644 index 0000000000..f076ee0fad --- /dev/null +++ b/packages/simplex-chat-nodejs/docs/api.Variable.defaultBotAddressSettings.md @@ -0,0 +1,11 @@ +[**simplex-chat**](README.md) + +*** + +[simplex-chat](README.md) / [api](Namespace.api.md) / defaultBotAddressSettings + +# Variable: defaultBotAddressSettings + +> `const` **defaultBotAddressSettings**: [`BotAddressSettings`](api.Interface.BotAddressSettings.md) + +Defined in: [src/api.ts:44](../src/api.ts#L44) diff --git a/packages/simplex-chat-nodejs/docs/bot.Function.run.md b/packages/simplex-chat-nodejs/docs/bot.Function.run.md new file mode 100644 index 0000000000..bc31ad01a8 --- /dev/null +++ b/packages/simplex-chat-nodejs/docs/bot.Function.run.md @@ -0,0 +1,21 @@ +[**simplex-chat**](README.md) + +*** + +[simplex-chat](README.md) / [bot](Namespace.bot.md) / run + +# Function: run() + +> **run**(`__namedParameters`): `Promise`\<\[[`ChatApi`](api.Class.ChatApi.md), `User`, `UserContactLink` \| `undefined`\]\> + +Defined in: [src/bot.ts:49](../src/bot.ts#L49) + +## Parameters + +### \_\_namedParameters + +[`BotConfig`](bot.Interface.BotConfig.md) + +## Returns + +`Promise`\<\[[`ChatApi`](api.Class.ChatApi.md), `User`, `UserContactLink` \| `undefined`\]\> diff --git a/packages/simplex-chat-nodejs/docs/bot.Interface.BotConfig.md b/packages/simplex-chat-nodejs/docs/bot.Interface.BotConfig.md new file mode 100644 index 0000000000..0951c5a129 --- /dev/null +++ b/packages/simplex-chat-nodejs/docs/bot.Interface.BotConfig.md @@ -0,0 +1,75 @@ +[**simplex-chat**](README.md) + +*** + +[simplex-chat](README.md) / [bot](Namespace.bot.md) / BotConfig + +# Interface: BotConfig + +Defined in: [src/bot.ts:37](../src/bot.ts#L37) + +## Properties + +### dbOpts + +> **dbOpts**: [`BotDbOpts`](bot.Interface.BotDbOpts.md) + +Defined in: [src/bot.ts:39](../src/bot.ts#L39) + +*** + +### events? + +> `optional` **events**: [`EventSubscribers`](api.TypeAlias.EventSubscribers.md) + +Defined in: [src/bot.ts:46](../src/bot.ts#L46) + +*** + +### onCommands? + +> `optional` **onCommands**: \{\[`key`: `string`\]: (`chatItem`, `command`) => `void` \| `Promise`\<`void`\> \| `undefined`; \} + +Defined in: [src/bot.ts:43](../src/bot.ts#L43) + +#### Index Signature + +\[`key`: `string`\]: (`chatItem`, `command`) => `void` \| `Promise`\<`void`\> \| `undefined` + +*** + +### onMessage()? + +> `optional` **onMessage**: (`chatItem`, `content`) => `void` \| `Promise`\<`void`\> + +Defined in: [src/bot.ts:41](../src/bot.ts#L41) + +#### Parameters + +##### chatItem + +`AChatItem` + +##### content + +`MsgContent` + +#### Returns + +`void` \| `Promise`\<`void`\> + +*** + +### options + +> **options**: [`BotOptions`](bot.Interface.BotOptions.md) + +Defined in: [src/bot.ts:40](../src/bot.ts#L40) + +*** + +### profile + +> **profile**: `Profile` + +Defined in: [src/bot.ts:38](../src/bot.ts#L38) diff --git a/packages/simplex-chat-nodejs/docs/bot.Interface.BotDbOpts.md b/packages/simplex-chat-nodejs/docs/bot.Interface.BotDbOpts.md new file mode 100644 index 0000000000..7a9f113f6a --- /dev/null +++ b/packages/simplex-chat-nodejs/docs/bot.Interface.BotDbOpts.md @@ -0,0 +1,33 @@ +[**simplex-chat**](README.md) + +*** + +[simplex-chat](README.md) / [bot](Namespace.bot.md) / BotDbOpts + +# Interface: BotDbOpts + +Defined in: [src/bot.ts:7](../src/bot.ts#L7) + +## Properties + +### confirmMigrations? + +> `optional` **confirmMigrations**: [`MigrationConfirmation`](core.Enumeration.MigrationConfirmation.md) + +Defined in: [src/bot.ts:10](../src/bot.ts#L10) + +*** + +### dbFilePrefix + +> **dbFilePrefix**: `string` + +Defined in: [src/bot.ts:8](../src/bot.ts#L8) + +*** + +### dbKey? + +> `optional` **dbKey**: `string` + +Defined in: [src/bot.ts:9](../src/bot.ts#L9) diff --git a/packages/simplex-chat-nodejs/docs/bot.Interface.BotOptions.md b/packages/simplex-chat-nodejs/docs/bot.Interface.BotOptions.md new file mode 100644 index 0000000000..44d4380e5a --- /dev/null +++ b/packages/simplex-chat-nodejs/docs/bot.Interface.BotOptions.md @@ -0,0 +1,81 @@ +[**simplex-chat**](README.md) + +*** + +[simplex-chat](README.md) / [bot](Namespace.bot.md) / BotOptions + +# Interface: BotOptions + +Defined in: [src/bot.ts:13](../src/bot.ts#L13) + +## Properties + +### addressSettings? + +> `optional` **addressSettings**: [`BotAddressSettings`](api.Interface.BotAddressSettings.md) + +Defined in: [src/bot.ts:17](../src/bot.ts#L17) + +*** + +### allowFiles? + +> `optional` **allowFiles**: `boolean` + +Defined in: [src/bot.ts:18](../src/bot.ts#L18) + +*** + +### commands? + +> `optional` **commands**: `ChatBotCommand`[] + +Defined in: [src/bot.ts:19](../src/bot.ts#L19) + +*** + +### createAddress? + +> `optional` **createAddress**: `boolean` + +Defined in: [src/bot.ts:14](../src/bot.ts#L14) + +*** + +### logContacts? + +> `optional` **logContacts**: `boolean` + +Defined in: [src/bot.ts:21](../src/bot.ts#L21) + +*** + +### logNetwork? + +> `optional` **logNetwork**: `boolean` + +Defined in: [src/bot.ts:22](../src/bot.ts#L22) + +*** + +### updateAddress? + +> `optional` **updateAddress**: `boolean` + +Defined in: [src/bot.ts:15](../src/bot.ts#L15) + +*** + +### updateProfile? + +> `optional` **updateProfile**: `boolean` + +Defined in: [src/bot.ts:16](../src/bot.ts#L16) + +*** + +### useBotProfile? + +> `optional` **useBotProfile**: `boolean` + +Defined in: [src/bot.ts:20](../src/bot.ts#L20) diff --git a/packages/simplex-chat-nodejs/docs/core.Class.ChatAPIError.md b/packages/simplex-chat-nodejs/docs/core.Class.ChatAPIError.md new file mode 100644 index 0000000000..c6082f2985 --- /dev/null +++ b/packages/simplex-chat-nodejs/docs/core.Class.ChatAPIError.md @@ -0,0 +1,205 @@ +[**simplex-chat**](README.md) + +*** + +[simplex-chat](README.md) / [core](Namespace.core.md) / ChatAPIError + +# Class: ChatAPIError + +Defined in: [src/core.ts:92](../src/core.ts#L92) + +## Extends + +- `Error` + +## Constructors + +### Constructor + +> **new ChatAPIError**(`message`, `chatError`): `ChatAPIError` + +Defined in: [src/core.ts:93](../src/core.ts#L93) + +#### Parameters + +##### message + +`string` + +##### chatError + +`ChatError` | `undefined` + +#### Returns + +`ChatAPIError` + +#### Overrides + +`Error.constructor` + +## Properties + +### chatError + +> **chatError**: `ChatError` \| `undefined` = `undefined` + +Defined in: [src/core.ts:93](../src/core.ts#L93) + +*** + +### message + +> **message**: `string` + +Defined in: [src/core.ts:93](../src/core.ts#L93) + +#### Inherited from + +`Error.message` + +*** + +### name + +> **name**: `string` + +Defined in: [node\_modules/typescript/lib/lib.es5.d.ts:1076](../node_modules/typescript/lib/lib.es5.d.ts#L1076) + +#### Inherited from + +`Error.name` + +*** + +### stack? + +> `optional` **stack**: `string` + +Defined in: [node\_modules/typescript/lib/lib.es5.d.ts:1078](../node_modules/typescript/lib/lib.es5.d.ts#L1078) + +#### Inherited from + +`Error.stack` + +*** + +### stackTraceLimit + +> `static` **stackTraceLimit**: `number` + +Defined in: [node\_modules/@types/node/globals.d.ts:67](../node_modules/@types/node/globals.d.ts#L67) + +The `Error.stackTraceLimit` property specifies the number of stack frames +collected by a stack trace (whether generated by `new Error().stack` or +`Error.captureStackTrace(obj)`). + +The default value is `10` but may be set to any valid JavaScript number. Changes +will affect any stack trace captured _after_ the value has been changed. + +If set to a non-number value, or set to a negative number, stack traces will +not capture any frames. + +#### Inherited from + +`Error.stackTraceLimit` + +## Methods + +### captureStackTrace() + +> `static` **captureStackTrace**(`targetObject`, `constructorOpt?`): `void` + +Defined in: [node\_modules/@types/node/globals.d.ts:51](../node_modules/@types/node/globals.d.ts#L51) + +Creates a `.stack` property on `targetObject`, which when accessed returns +a string representing the location in the code at which +`Error.captureStackTrace()` was called. + +```js +const myObject = {}; +Error.captureStackTrace(myObject); +myObject.stack; // Similar to `new Error().stack` +``` + +The first line of the trace will be prefixed with +`${myObject.name}: ${myObject.message}`. + +The optional `constructorOpt` argument accepts a function. If given, all frames +above `constructorOpt`, including `constructorOpt`, will be omitted from the +generated stack trace. + +The `constructorOpt` argument is useful for hiding implementation +details of error generation from the user. For instance: + +```js +function a() { + b(); +} + +function b() { + c(); +} + +function c() { + // Create an error without stack trace to avoid calculating the stack trace twice. + const { stackTraceLimit } = Error; + Error.stackTraceLimit = 0; + const error = new Error(); + Error.stackTraceLimit = stackTraceLimit; + + // Capture the stack trace above function b + Error.captureStackTrace(error, b); // Neither function c, nor b is included in the stack trace + throw error; +} + +a(); +``` + +#### Parameters + +##### targetObject + +`object` + +##### constructorOpt? + +`Function` + +#### Returns + +`void` + +#### Inherited from + +`Error.captureStackTrace` + +*** + +### prepareStackTrace() + +> `static` **prepareStackTrace**(`err`, `stackTraces`): `any` + +Defined in: [node\_modules/@types/node/globals.d.ts:55](../node_modules/@types/node/globals.d.ts#L55) + +#### Parameters + +##### err + +`Error` + +##### stackTraces + +`CallSite`[] + +#### Returns + +`any` + +#### See + +https://v8.dev/docs/stack-trace-api#customizing-stack-traces + +#### Inherited from + +`Error.prepareStackTrace` diff --git a/packages/simplex-chat-nodejs/docs/core.Class.ChatInitError.md b/packages/simplex-chat-nodejs/docs/core.Class.ChatInitError.md new file mode 100644 index 0000000000..eff5123fb0 --- /dev/null +++ b/packages/simplex-chat-nodejs/docs/core.Class.ChatInitError.md @@ -0,0 +1,205 @@ +[**simplex-chat**](README.md) + +*** + +[simplex-chat](README.md) / [core](Namespace.core.md) / ChatInitError + +# Class: ChatInitError + +Defined in: [src/core.ts:116](../src/core.ts#L116) + +## Extends + +- `Error` + +## Constructors + +### Constructor + +> **new ChatInitError**(`message`, `dbMigrationError`): `ChatInitError` + +Defined in: [src/core.ts:117](../src/core.ts#L117) + +#### Parameters + +##### message + +`string` + +##### dbMigrationError + +[`DBMigrationError`](core.TypeAlias.DBMigrationError.md) + +#### Returns + +`ChatInitError` + +#### Overrides + +`Error.constructor` + +## Properties + +### dbMigrationError + +> **dbMigrationError**: [`DBMigrationError`](core.TypeAlias.DBMigrationError.md) + +Defined in: [src/core.ts:117](../src/core.ts#L117) + +*** + +### message + +> **message**: `string` + +Defined in: [src/core.ts:117](../src/core.ts#L117) + +#### Inherited from + +`Error.message` + +*** + +### name + +> **name**: `string` + +Defined in: [node\_modules/typescript/lib/lib.es5.d.ts:1076](../node_modules/typescript/lib/lib.es5.d.ts#L1076) + +#### Inherited from + +`Error.name` + +*** + +### stack? + +> `optional` **stack**: `string` + +Defined in: [node\_modules/typescript/lib/lib.es5.d.ts:1078](../node_modules/typescript/lib/lib.es5.d.ts#L1078) + +#### Inherited from + +`Error.stack` + +*** + +### stackTraceLimit + +> `static` **stackTraceLimit**: `number` + +Defined in: [node\_modules/@types/node/globals.d.ts:67](../node_modules/@types/node/globals.d.ts#L67) + +The `Error.stackTraceLimit` property specifies the number of stack frames +collected by a stack trace (whether generated by `new Error().stack` or +`Error.captureStackTrace(obj)`). + +The default value is `10` but may be set to any valid JavaScript number. Changes +will affect any stack trace captured _after_ the value has been changed. + +If set to a non-number value, or set to a negative number, stack traces will +not capture any frames. + +#### Inherited from + +`Error.stackTraceLimit` + +## Methods + +### captureStackTrace() + +> `static` **captureStackTrace**(`targetObject`, `constructorOpt?`): `void` + +Defined in: [node\_modules/@types/node/globals.d.ts:51](../node_modules/@types/node/globals.d.ts#L51) + +Creates a `.stack` property on `targetObject`, which when accessed returns +a string representing the location in the code at which +`Error.captureStackTrace()` was called. + +```js +const myObject = {}; +Error.captureStackTrace(myObject); +myObject.stack; // Similar to `new Error().stack` +``` + +The first line of the trace will be prefixed with +`${myObject.name}: ${myObject.message}`. + +The optional `constructorOpt` argument accepts a function. If given, all frames +above `constructorOpt`, including `constructorOpt`, will be omitted from the +generated stack trace. + +The `constructorOpt` argument is useful for hiding implementation +details of error generation from the user. For instance: + +```js +function a() { + b(); +} + +function b() { + c(); +} + +function c() { + // Create an error without stack trace to avoid calculating the stack trace twice. + const { stackTraceLimit } = Error; + Error.stackTraceLimit = 0; + const error = new Error(); + Error.stackTraceLimit = stackTraceLimit; + + // Capture the stack trace above function b + Error.captureStackTrace(error, b); // Neither function c, nor b is included in the stack trace + throw error; +} + +a(); +``` + +#### Parameters + +##### targetObject + +`object` + +##### constructorOpt? + +`Function` + +#### Returns + +`void` + +#### Inherited from + +`Error.captureStackTrace` + +*** + +### prepareStackTrace() + +> `static` **prepareStackTrace**(`err`, `stackTraces`): `any` + +Defined in: [node\_modules/@types/node/globals.d.ts:55](../node_modules/@types/node/globals.d.ts#L55) + +#### Parameters + +##### err + +`Error` + +##### stackTraces + +`CallSite`[] + +#### Returns + +`any` + +#### See + +https://v8.dev/docs/stack-trace-api#customizing-stack-traces + +#### Inherited from + +`Error.prepareStackTrace` diff --git a/packages/simplex-chat-nodejs/docs/core.DBMigrationError.Interface.ErrorMigration.md b/packages/simplex-chat-nodejs/docs/core.DBMigrationError.Interface.ErrorMigration.md new file mode 100644 index 0000000000..02cf84b763 --- /dev/null +++ b/packages/simplex-chat-nodejs/docs/core.DBMigrationError.Interface.ErrorMigration.md @@ -0,0 +1,41 @@ +[**simplex-chat**](README.md) + +*** + +[simplex-chat](README.md) / [core](Namespace.core.md) / [DBMigrationError](core.Namespace.DBMigrationError.md) / ErrorMigration + +# Interface: ErrorMigration + +Defined in: [src/core.ts:144](../src/core.ts#L144) + +## Extends + +- `Interface` + +## Properties + +### dbFile + +> **dbFile**: `string` + +Defined in: [src/core.ts:146](../src/core.ts#L146) + +*** + +### migrationError + +> **migrationError**: [`MigrationError`](core.TypeAlias.MigrationError.md) + +Defined in: [src/core.ts:147](../src/core.ts#L147) + +*** + +### type + +> **type**: `"errorMigration"` + +Defined in: [src/core.ts:145](../src/core.ts#L145) + +#### Overrides + +`Interface.type` diff --git a/packages/simplex-chat-nodejs/docs/core.DBMigrationError.Interface.ErrorNotADatabase.md b/packages/simplex-chat-nodejs/docs/core.DBMigrationError.Interface.ErrorNotADatabase.md new file mode 100644 index 0000000000..18e2429081 --- /dev/null +++ b/packages/simplex-chat-nodejs/docs/core.DBMigrationError.Interface.ErrorNotADatabase.md @@ -0,0 +1,33 @@ +[**simplex-chat**](README.md) + +*** + +[simplex-chat](README.md) / [core](Namespace.core.md) / [DBMigrationError](core.Namespace.DBMigrationError.md) / ErrorNotADatabase + +# Interface: ErrorNotADatabase + +Defined in: [src/core.ts:139](../src/core.ts#L139) + +## Extends + +- `Interface` + +## Properties + +### dbFile + +> **dbFile**: `string` + +Defined in: [src/core.ts:141](../src/core.ts#L141) + +*** + +### type + +> **type**: `"errorNotADatabase"` + +Defined in: [src/core.ts:140](../src/core.ts#L140) + +#### Overrides + +`Interface.type` diff --git a/packages/simplex-chat-nodejs/docs/core.DBMigrationError.Interface.ErrorSQL.md b/packages/simplex-chat-nodejs/docs/core.DBMigrationError.Interface.ErrorSQL.md new file mode 100644 index 0000000000..4d85b04197 --- /dev/null +++ b/packages/simplex-chat-nodejs/docs/core.DBMigrationError.Interface.ErrorSQL.md @@ -0,0 +1,41 @@ +[**simplex-chat**](README.md) + +*** + +[simplex-chat](README.md) / [core](Namespace.core.md) / [DBMigrationError](core.Namespace.DBMigrationError.md) / ErrorSQL + +# Interface: ErrorSQL + +Defined in: [src/core.ts:150](../src/core.ts#L150) + +## Extends + +- `Interface` + +## Properties + +### dbFile + +> **dbFile**: `string` + +Defined in: [src/core.ts:152](../src/core.ts#L152) + +*** + +### migrationSQLError + +> **migrationSQLError**: `string` + +Defined in: [src/core.ts:153](../src/core.ts#L153) + +*** + +### type + +> **type**: `"errorSQL"` + +Defined in: [src/core.ts:151](../src/core.ts#L151) + +#### Overrides + +`Interface.type` diff --git a/packages/simplex-chat-nodejs/docs/core.DBMigrationError.Interface.InvalidConfirmation.md b/packages/simplex-chat-nodejs/docs/core.DBMigrationError.Interface.InvalidConfirmation.md new file mode 100644 index 0000000000..34dea63aed --- /dev/null +++ b/packages/simplex-chat-nodejs/docs/core.DBMigrationError.Interface.InvalidConfirmation.md @@ -0,0 +1,25 @@ +[**simplex-chat**](README.md) + +*** + +[simplex-chat](README.md) / [core](Namespace.core.md) / [DBMigrationError](core.Namespace.DBMigrationError.md) / InvalidConfirmation + +# Interface: InvalidConfirmation + +Defined in: [src/core.ts:135](../src/core.ts#L135) + +## Extends + +- `Interface` + +## Properties + +### type + +> **type**: `"invalidConfirmation"` + +Defined in: [src/core.ts:136](../src/core.ts#L136) + +#### Overrides + +`Interface.type` diff --git a/packages/simplex-chat-nodejs/docs/core.DBMigrationError.TypeAlias.Tag.md b/packages/simplex-chat-nodejs/docs/core.DBMigrationError.TypeAlias.Tag.md new file mode 100644 index 0000000000..a3ef341601 --- /dev/null +++ b/packages/simplex-chat-nodejs/docs/core.DBMigrationError.TypeAlias.Tag.md @@ -0,0 +1,11 @@ +[**simplex-chat**](README.md) + +*** + +[simplex-chat](README.md) / [core](Namespace.core.md) / [DBMigrationError](core.Namespace.DBMigrationError.md) / Tag + +# Type Alias: Tag + +> **Tag** = `"invalidConfirmation"` \| `"errorNotADatabase"` \| `"errorMigration"` \| `"errorSQL"` + +Defined in: [src/core.ts:129](../src/core.ts#L129) diff --git a/packages/simplex-chat-nodejs/docs/core.Enumeration.MigrationConfirmation.md b/packages/simplex-chat-nodejs/docs/core.Enumeration.MigrationConfirmation.md new file mode 100644 index 0000000000..7dfd4991bf --- /dev/null +++ b/packages/simplex-chat-nodejs/docs/core.Enumeration.MigrationConfirmation.md @@ -0,0 +1,43 @@ +[**simplex-chat**](README.md) + +*** + +[simplex-chat](README.md) / [core](Namespace.core.md) / MigrationConfirmation + +# Enumeration: MigrationConfirmation + +Defined in: [src/core.ts:101](../src/core.ts#L101) + +Migration confirmation mode + +## Enumeration Members + +### Console + +> **Console**: `"console"` + +Defined in: [src/core.ts:104](../src/core.ts#L104) + +*** + +### Error + +> **Error**: `"error"` + +Defined in: [src/core.ts:105](../src/core.ts#L105) + +*** + +### YesUp + +> **YesUp**: `"yesUp"` + +Defined in: [src/core.ts:102](../src/core.ts#L102) + +*** + +### YesUpDown + +> **YesUpDown**: `"yesUpDown"` + +Defined in: [src/core.ts:103](../src/core.ts#L103) diff --git a/packages/simplex-chat-nodejs/docs/core.Function.chatCloseStore.md b/packages/simplex-chat-nodejs/docs/core.Function.chatCloseStore.md new file mode 100644 index 0000000000..deeb3213fd --- /dev/null +++ b/packages/simplex-chat-nodejs/docs/core.Function.chatCloseStore.md @@ -0,0 +1,23 @@ +[**simplex-chat**](README.md) + +*** + +[simplex-chat](README.md) / [core](Namespace.core.md) / chatCloseStore + +# Function: chatCloseStore() + +> **chatCloseStore**(`ctrl`): `Promise`\<`void`\> + +Defined in: [src/core.ts:17](../src/core.ts#L17) + +Close chat store + +## Parameters + +### ctrl + +`bigint` + +## Returns + +`Promise`\<`void`\> diff --git a/packages/simplex-chat-nodejs/docs/core.Function.chatDecryptFile.md b/packages/simplex-chat-nodejs/docs/core.Function.chatDecryptFile.md new file mode 100644 index 0000000000..434aeeaae8 --- /dev/null +++ b/packages/simplex-chat-nodejs/docs/core.Function.chatDecryptFile.md @@ -0,0 +1,31 @@ +[**simplex-chat**](README.md) + +*** + +[simplex-chat](README.md) / [core](Namespace.core.md) / chatDecryptFile + +# Function: chatDecryptFile() + +> **chatDecryptFile**(`fromPath`, `__namedParameters`, `toPath`): `Promise`\<`void`\> + +Defined in: [src/core.ts:73](../src/core.ts#L73) + +Decrypt file + +## Parameters + +### fromPath + +`string` + +### \_\_namedParameters + +[`CryptoArgs`](core.Interface.CryptoArgs.md) + +### toPath + +`string` + +## Returns + +`Promise`\<`void`\> diff --git a/packages/simplex-chat-nodejs/docs/core.Function.chatEncryptFile.md b/packages/simplex-chat-nodejs/docs/core.Function.chatEncryptFile.md new file mode 100644 index 0000000000..6aa0ad2923 --- /dev/null +++ b/packages/simplex-chat-nodejs/docs/core.Function.chatEncryptFile.md @@ -0,0 +1,31 @@ +[**simplex-chat**](README.md) + +*** + +[simplex-chat](README.md) / [core](Namespace.core.md) / chatEncryptFile + +# Function: chatEncryptFile() + +> **chatEncryptFile**(`ctrl`, `fromPath`, `toPath`): `Promise`\<[`CryptoArgs`](core.Interface.CryptoArgs.md)\> + +Defined in: [src/core.ts:65](../src/core.ts#L65) + +Encrypt file + +## Parameters + +### ctrl + +`bigint` + +### fromPath + +`string` + +### toPath + +`string` + +## Returns + +`Promise`\<[`CryptoArgs`](core.Interface.CryptoArgs.md)\> diff --git a/packages/simplex-chat-nodejs/docs/core.Function.chatMigrateInit.md b/packages/simplex-chat-nodejs/docs/core.Function.chatMigrateInit.md new file mode 100644 index 0000000000..9116026f56 --- /dev/null +++ b/packages/simplex-chat-nodejs/docs/core.Function.chatMigrateInit.md @@ -0,0 +1,31 @@ +[**simplex-chat**](README.md) + +*** + +[simplex-chat](README.md) / [core](Namespace.core.md) / chatMigrateInit + +# Function: chatMigrateInit() + +> **chatMigrateInit**(`dbPath`, `dbKey`, `confirm`): `Promise`\<`bigint`\> + +Defined in: [src/core.ts:7](../src/core.ts#L7) + +Initialize chat controller + +## Parameters + +### dbPath + +`string` + +### dbKey + +`string` + +### confirm + +[`MigrationConfirmation`](core.Enumeration.MigrationConfirmation.md) + +## Returns + +`Promise`\<`bigint`\> diff --git a/packages/simplex-chat-nodejs/docs/core.Function.chatReadFile.md b/packages/simplex-chat-nodejs/docs/core.Function.chatReadFile.md new file mode 100644 index 0000000000..27de43e63c --- /dev/null +++ b/packages/simplex-chat-nodejs/docs/core.Function.chatReadFile.md @@ -0,0 +1,27 @@ +[**simplex-chat**](README.md) + +*** + +[simplex-chat](README.md) / [core](Namespace.core.md) / chatReadFile + +# Function: chatReadFile() + +> **chatReadFile**(`path`, `__namedParameters`): `Promise`\<`ArrayBuffer`\> + +Defined in: [src/core.ts:58](../src/core.ts#L58) + +Read buffer from encrypted file + +## Parameters + +### path + +`string` + +### \_\_namedParameters + +[`CryptoArgs`](core.Interface.CryptoArgs.md) + +## Returns + +`Promise`\<`ArrayBuffer`\> diff --git a/packages/simplex-chat-nodejs/docs/core.Function.chatRecvMsgWait.md b/packages/simplex-chat-nodejs/docs/core.Function.chatRecvMsgWait.md new file mode 100644 index 0000000000..9bf44d6523 --- /dev/null +++ b/packages/simplex-chat-nodejs/docs/core.Function.chatRecvMsgWait.md @@ -0,0 +1,27 @@ +[**simplex-chat**](README.md) + +*** + +[simplex-chat](README.md) / [core](Namespace.core.md) / chatRecvMsgWait + +# Function: chatRecvMsgWait() + +> **chatRecvMsgWait**(`ctrl`, `wait`): `Promise`\<`ChatEvent` \| `undefined`\> + +Defined in: [src/core.ts:37](../src/core.ts#L37) + +Receive chat event + +## Parameters + +### ctrl + +`bigint` + +### wait + +`number` + +## Returns + +`Promise`\<`ChatEvent` \| `undefined`\> diff --git a/packages/simplex-chat-nodejs/docs/core.Function.chatSendCmd.md b/packages/simplex-chat-nodejs/docs/core.Function.chatSendCmd.md new file mode 100644 index 0000000000..2dfcba45b4 --- /dev/null +++ b/packages/simplex-chat-nodejs/docs/core.Function.chatSendCmd.md @@ -0,0 +1,27 @@ +[**simplex-chat**](README.md) + +*** + +[simplex-chat](README.md) / [core](Namespace.core.md) / chatSendCmd + +# Function: chatSendCmd() + +> **chatSendCmd**(`ctrl`, `cmd`): `Promise`\<`ChatResponse`\> + +Defined in: [src/core.ts:25](../src/core.ts#L25) + +Send chat command as string + +## Parameters + +### ctrl + +`bigint` + +### cmd + +`string` + +## Returns + +`Promise`\<`ChatResponse`\> diff --git a/packages/simplex-chat-nodejs/docs/core.Function.chatWriteFile.md b/packages/simplex-chat-nodejs/docs/core.Function.chatWriteFile.md new file mode 100644 index 0000000000..3b1d770fbb --- /dev/null +++ b/packages/simplex-chat-nodejs/docs/core.Function.chatWriteFile.md @@ -0,0 +1,31 @@ +[**simplex-chat**](README.md) + +*** + +[simplex-chat](README.md) / [core](Namespace.core.md) / chatWriteFile + +# Function: chatWriteFile() + +> **chatWriteFile**(`ctrl`, `path`, `buffer`): `Promise`\<[`CryptoArgs`](core.Interface.CryptoArgs.md)\> + +Defined in: [src/core.ts:50](../src/core.ts#L50) + +Write buffer to encrypted file + +## Parameters + +### ctrl + +`bigint` + +### path + +`string` + +### buffer + +`ArrayBuffer` + +## Returns + +`Promise`\<[`CryptoArgs`](core.Interface.CryptoArgs.md)\> diff --git a/packages/simplex-chat-nodejs/docs/core.Interface.APIResult.md b/packages/simplex-chat-nodejs/docs/core.Interface.APIResult.md new file mode 100644 index 0000000000..906ef3ec3e --- /dev/null +++ b/packages/simplex-chat-nodejs/docs/core.Interface.APIResult.md @@ -0,0 +1,31 @@ +[**simplex-chat**](README.md) + +*** + +[simplex-chat](README.md) / [core](Namespace.core.md) / APIResult + +# Interface: APIResult\ + +Defined in: [src/core.ts:87](../src/core.ts#L87) + +## Type Parameters + +### R + +`R` + +## Properties + +### error? + +> `optional` **error**: `ChatError` + +Defined in: [src/core.ts:89](../src/core.ts#L89) + +*** + +### result? + +> `optional` **result**: `R` + +Defined in: [src/core.ts:88](../src/core.ts#L88) diff --git a/packages/simplex-chat-nodejs/docs/core.Interface.CryptoArgs.md b/packages/simplex-chat-nodejs/docs/core.Interface.CryptoArgs.md new file mode 100644 index 0000000000..eddcb0bc5a --- /dev/null +++ b/packages/simplex-chat-nodejs/docs/core.Interface.CryptoArgs.md @@ -0,0 +1,27 @@ +[**simplex-chat**](README.md) + +*** + +[simplex-chat](README.md) / [core](Namespace.core.md) / CryptoArgs + +# Interface: CryptoArgs + +Defined in: [src/core.ts:111](../src/core.ts#L111) + +File encryption key and nonce + +## Properties + +### fileKey + +> **fileKey**: `string` + +Defined in: [src/core.ts:112](../src/core.ts#L112) + +*** + +### fileNonce + +> **fileNonce**: `string` + +Defined in: [src/core.ts:113](../src/core.ts#L113) diff --git a/packages/simplex-chat-nodejs/docs/core.Interface.UpMigration.md b/packages/simplex-chat-nodejs/docs/core.Interface.UpMigration.md new file mode 100644 index 0000000000..32f6c267aa --- /dev/null +++ b/packages/simplex-chat-nodejs/docs/core.Interface.UpMigration.md @@ -0,0 +1,25 @@ +[**simplex-chat**](README.md) + +*** + +[simplex-chat](README.md) / [core](Namespace.core.md) / UpMigration + +# Interface: UpMigration + +Defined in: [src/core.ts:185](../src/core.ts#L185) + +## Properties + +### upName + +> **upName**: `string` + +Defined in: [src/core.ts:186](../src/core.ts#L186) + +*** + +### withDown + +> **withDown**: `boolean` + +Defined in: [src/core.ts:187](../src/core.ts#L187) diff --git a/packages/simplex-chat-nodejs/docs/core.MTRError.Interface.MTREDifferent.md b/packages/simplex-chat-nodejs/docs/core.MTRError.Interface.MTREDifferent.md new file mode 100644 index 0000000000..8dab81a3a3 --- /dev/null +++ b/packages/simplex-chat-nodejs/docs/core.MTRError.Interface.MTREDifferent.md @@ -0,0 +1,33 @@ +[**simplex-chat**](README.md) + +*** + +[simplex-chat](README.md) / [core](Namespace.core.md) / [MTRError](core.Namespace.MTRError.md) / MTREDifferent + +# Interface: MTREDifferent + +Defined in: [src/core.ts:206](../src/core.ts#L206) + +## Extends + +- `Interface` + +## Properties + +### downMigrations + +> **downMigrations**: `string`[] + +Defined in: [src/core.ts:208](../src/core.ts#L208) + +*** + +### type + +> **type**: `"different"` + +Defined in: [src/core.ts:207](../src/core.ts#L207) + +#### Overrides + +`Interface.type` diff --git a/packages/simplex-chat-nodejs/docs/core.MTRError.Interface.MTRENoDown.md b/packages/simplex-chat-nodejs/docs/core.MTRError.Interface.MTRENoDown.md new file mode 100644 index 0000000000..1de634e40e --- /dev/null +++ b/packages/simplex-chat-nodejs/docs/core.MTRError.Interface.MTRENoDown.md @@ -0,0 +1,33 @@ +[**simplex-chat**](README.md) + +*** + +[simplex-chat](README.md) / [core](Namespace.core.md) / [MTRError](core.Namespace.MTRError.md) / MTRENoDown + +# Interface: MTRENoDown + +Defined in: [src/core.ts:201](../src/core.ts#L201) + +## Extends + +- `Interface` + +## Properties + +### type + +> **type**: `"noDown"` + +Defined in: [src/core.ts:202](../src/core.ts#L202) + +#### Overrides + +`Interface.type` + +*** + +### upMigrations + +> **upMigrations**: [`UpMigration`](core.Interface.UpMigration.md) + +Defined in: [src/core.ts:203](../src/core.ts#L203) diff --git a/packages/simplex-chat-nodejs/docs/core.MTRError.TypeAlias.Tag.md b/packages/simplex-chat-nodejs/docs/core.MTRError.TypeAlias.Tag.md new file mode 100644 index 0000000000..768baa0068 --- /dev/null +++ b/packages/simplex-chat-nodejs/docs/core.MTRError.TypeAlias.Tag.md @@ -0,0 +1,11 @@ +[**simplex-chat**](README.md) + +*** + +[simplex-chat](README.md) / [core](Namespace.core.md) / [MTRError](core.Namespace.MTRError.md) / Tag + +# Type Alias: Tag + +> **Tag** = `"noDown"` \| `"different"` + +Defined in: [src/core.ts:195](../src/core.ts#L195) diff --git a/packages/simplex-chat-nodejs/docs/core.MigrationError.Interface.MEDowngrade.md b/packages/simplex-chat-nodejs/docs/core.MigrationError.Interface.MEDowngrade.md new file mode 100644 index 0000000000..a2742cbff2 --- /dev/null +++ b/packages/simplex-chat-nodejs/docs/core.MigrationError.Interface.MEDowngrade.md @@ -0,0 +1,33 @@ +[**simplex-chat**](README.md) + +*** + +[simplex-chat](README.md) / [core](Namespace.core.md) / [MigrationError](core.Namespace.MigrationError.md) / MEDowngrade + +# Interface: MEDowngrade + +Defined in: [src/core.ts:174](../src/core.ts#L174) + +## Extends + +- `Interface` + +## Properties + +### downMigrations + +> **downMigrations**: `string`[] + +Defined in: [src/core.ts:176](../src/core.ts#L176) + +*** + +### type + +> **type**: `"downgrade"` + +Defined in: [src/core.ts:175](../src/core.ts#L175) + +#### Overrides + +`Interface.type` diff --git a/packages/simplex-chat-nodejs/docs/core.MigrationError.Interface.MEUpgrade.md b/packages/simplex-chat-nodejs/docs/core.MigrationError.Interface.MEUpgrade.md new file mode 100644 index 0000000000..08fe7d56e3 --- /dev/null +++ b/packages/simplex-chat-nodejs/docs/core.MigrationError.Interface.MEUpgrade.md @@ -0,0 +1,33 @@ +[**simplex-chat**](README.md) + +*** + +[simplex-chat](README.md) / [core](Namespace.core.md) / [MigrationError](core.Namespace.MigrationError.md) / MEUpgrade + +# Interface: MEUpgrade + +Defined in: [src/core.ts:169](../src/core.ts#L169) + +## Extends + +- `Interface` + +## Properties + +### type + +> **type**: `"upgrade"` + +Defined in: [src/core.ts:170](../src/core.ts#L170) + +#### Overrides + +`Interface.type` + +*** + +### upMigrations + +> **upMigrations**: [`UpMigration`](core.Interface.UpMigration.md) + +Defined in: [src/core.ts:171](../src/core.ts#L171) diff --git a/packages/simplex-chat-nodejs/docs/core.MigrationError.Interface.MigrationError.md b/packages/simplex-chat-nodejs/docs/core.MigrationError.Interface.MigrationError.md new file mode 100644 index 0000000000..cd811a5747 --- /dev/null +++ b/packages/simplex-chat-nodejs/docs/core.MigrationError.Interface.MigrationError.md @@ -0,0 +1,33 @@ +[**simplex-chat**](README.md) + +*** + +[simplex-chat](README.md) / [core](Namespace.core.md) / [MigrationError](core.Namespace.MigrationError.md) / MigrationError + +# Interface: MigrationError + +Defined in: [src/core.ts:179](../src/core.ts#L179) + +## Extends + +- `Interface` + +## Properties + +### mtrError + +> **mtrError**: [`MTRError`](core.TypeAlias.MTRError.md) + +Defined in: [src/core.ts:181](../src/core.ts#L181) + +*** + +### type + +> **type**: `"migrationError"` + +Defined in: [src/core.ts:180](../src/core.ts#L180) + +#### Overrides + +`Interface.type` diff --git a/packages/simplex-chat-nodejs/docs/core.MigrationError.TypeAlias.Tag.md b/packages/simplex-chat-nodejs/docs/core.MigrationError.TypeAlias.Tag.md new file mode 100644 index 0000000000..5ef6e70b08 --- /dev/null +++ b/packages/simplex-chat-nodejs/docs/core.MigrationError.TypeAlias.Tag.md @@ -0,0 +1,11 @@ +[**simplex-chat**](README.md) + +*** + +[simplex-chat](README.md) / [core](Namespace.core.md) / [MigrationError](core.Namespace.MigrationError.md) / Tag + +# Type Alias: Tag + +> **Tag** = `"upgrade"` \| `"downgrade"` \| `"migrationError"` + +Defined in: [src/core.ts:163](../src/core.ts#L163) diff --git a/packages/simplex-chat-nodejs/docs/core.Namespace.DBMigrationError.md b/packages/simplex-chat-nodejs/docs/core.Namespace.DBMigrationError.md new file mode 100644 index 0000000000..95ddfa5b24 --- /dev/null +++ b/packages/simplex-chat-nodejs/docs/core.Namespace.DBMigrationError.md @@ -0,0 +1,18 @@ +[**simplex-chat**](README.md) + +*** + +[simplex-chat](README.md) / [core](Namespace.core.md) / DBMigrationError + +# DBMigrationError + +## Interfaces + +- [ErrorMigration](core.DBMigrationError.Interface.ErrorMigration.md) +- [ErrorNotADatabase](core.DBMigrationError.Interface.ErrorNotADatabase.md) +- [ErrorSQL](core.DBMigrationError.Interface.ErrorSQL.md) +- [InvalidConfirmation](core.DBMigrationError.Interface.InvalidConfirmation.md) + +## Type Aliases + +- [Tag](core.DBMigrationError.TypeAlias.Tag.md) diff --git a/packages/simplex-chat-nodejs/docs/core.Namespace.MTRError.md b/packages/simplex-chat-nodejs/docs/core.Namespace.MTRError.md new file mode 100644 index 0000000000..70baca917a --- /dev/null +++ b/packages/simplex-chat-nodejs/docs/core.Namespace.MTRError.md @@ -0,0 +1,16 @@ +[**simplex-chat**](README.md) + +*** + +[simplex-chat](README.md) / [core](Namespace.core.md) / MTRError + +# MTRError + +## Interfaces + +- [MTREDifferent](core.MTRError.Interface.MTREDifferent.md) +- [MTRENoDown](core.MTRError.Interface.MTRENoDown.md) + +## Type Aliases + +- [Tag](core.MTRError.TypeAlias.Tag.md) diff --git a/packages/simplex-chat-nodejs/docs/core.Namespace.MigrationError.md b/packages/simplex-chat-nodejs/docs/core.Namespace.MigrationError.md new file mode 100644 index 0000000000..cc66142dbc --- /dev/null +++ b/packages/simplex-chat-nodejs/docs/core.Namespace.MigrationError.md @@ -0,0 +1,17 @@ +[**simplex-chat**](README.md) + +*** + +[simplex-chat](README.md) / [core](Namespace.core.md) / MigrationError + +# MigrationError + +## Interfaces + +- [MEDowngrade](core.MigrationError.Interface.MEDowngrade.md) +- [MEUpgrade](core.MigrationError.Interface.MEUpgrade.md) +- [MigrationError](core.MigrationError.Interface.MigrationError.md) + +## Type Aliases + +- [Tag](core.MigrationError.TypeAlias.Tag.md) diff --git a/packages/simplex-chat-nodejs/docs/core.TypeAlias.DBMigrationError.md b/packages/simplex-chat-nodejs/docs/core.TypeAlias.DBMigrationError.md new file mode 100644 index 0000000000..6473b3ef60 --- /dev/null +++ b/packages/simplex-chat-nodejs/docs/core.TypeAlias.DBMigrationError.md @@ -0,0 +1,11 @@ +[**simplex-chat**](README.md) + +*** + +[simplex-chat](README.md) / [core](Namespace.core.md) / DBMigrationError + +# Type Alias: DBMigrationError + +> **DBMigrationError** = [`InvalidConfirmation`](core.DBMigrationError.Interface.InvalidConfirmation.md) \| [`ErrorNotADatabase`](core.DBMigrationError.Interface.ErrorNotADatabase.md) \| [`ErrorMigration`](core.DBMigrationError.Interface.ErrorMigration.md) \| [`ErrorSQL`](core.DBMigrationError.Interface.ErrorSQL.md) + +Defined in: [src/core.ts:122](../src/core.ts#L122) diff --git a/packages/simplex-chat-nodejs/docs/core.TypeAlias.MTRError.md b/packages/simplex-chat-nodejs/docs/core.TypeAlias.MTRError.md new file mode 100644 index 0000000000..11aa5b7c24 --- /dev/null +++ b/packages/simplex-chat-nodejs/docs/core.TypeAlias.MTRError.md @@ -0,0 +1,11 @@ +[**simplex-chat**](README.md) + +*** + +[simplex-chat](README.md) / [core](Namespace.core.md) / MTRError + +# Type Alias: MTRError + +> **MTRError** = [`MTRENoDown`](core.MTRError.Interface.MTRENoDown.md) \| [`MTREDifferent`](core.MTRError.Interface.MTREDifferent.md) + +Defined in: [src/core.ts:190](../src/core.ts#L190) diff --git a/packages/simplex-chat-nodejs/docs/core.TypeAlias.MigrationError.md b/packages/simplex-chat-nodejs/docs/core.TypeAlias.MigrationError.md new file mode 100644 index 0000000000..c15b679769 --- /dev/null +++ b/packages/simplex-chat-nodejs/docs/core.TypeAlias.MigrationError.md @@ -0,0 +1,11 @@ +[**simplex-chat**](README.md) + +*** + +[simplex-chat](README.md) / [core](Namespace.core.md) / MigrationError + +# Type Alias: MigrationError + +> **MigrationError** = [`MEUpgrade`](core.MigrationError.Interface.MEUpgrade.md) \| [`MEDowngrade`](core.MigrationError.Interface.MEDowngrade.md) \| [`MigrationError`](core.MigrationError.Interface.MigrationError.md) + +Defined in: [src/core.ts:157](../src/core.ts#L157) diff --git a/packages/simplex-chat-nodejs/docs/util.Function.botAddressSettings.md b/packages/simplex-chat-nodejs/docs/util.Function.botAddressSettings.md new file mode 100644 index 0000000000..7fa5d81b7a --- /dev/null +++ b/packages/simplex-chat-nodejs/docs/util.Function.botAddressSettings.md @@ -0,0 +1,21 @@ +[**simplex-chat**](README.md) + +*** + +[simplex-chat](README.md) / [util](Namespace.util.md) / botAddressSettings + +# Function: botAddressSettings() + +> **botAddressSettings**(`__namedParameters`): [`BotAddressSettings`](api.Interface.BotAddressSettings.md) + +Defined in: [src/util.ts:48](../src/util.ts#L48) + +## Parameters + +### \_\_namedParameters + +`UserContactLink` + +## Returns + +[`BotAddressSettings`](api.Interface.BotAddressSettings.md) diff --git a/packages/simplex-chat-nodejs/docs/util.Function.chatInfoName.md b/packages/simplex-chat-nodejs/docs/util.Function.chatInfoName.md new file mode 100644 index 0000000000..dd68403c40 --- /dev/null +++ b/packages/simplex-chat-nodejs/docs/util.Function.chatInfoName.md @@ -0,0 +1,21 @@ +[**simplex-chat**](README.md) + +*** + +[simplex-chat](README.md) / [util](Namespace.util.md) / chatInfoName + +# Function: chatInfoName() + +> **chatInfoName**(`cInfo`): `string` + +Defined in: [src/util.ts:18](../src/util.ts#L18) + +## Parameters + +### cInfo + +`ChatInfo` + +## Returns + +`string` diff --git a/packages/simplex-chat-nodejs/docs/util.Function.chatInfoRef.md b/packages/simplex-chat-nodejs/docs/util.Function.chatInfoRef.md new file mode 100644 index 0000000000..dccb2a9d29 --- /dev/null +++ b/packages/simplex-chat-nodejs/docs/util.Function.chatInfoRef.md @@ -0,0 +1,21 @@ +[**simplex-chat**](README.md) + +*** + +[simplex-chat](README.md) / [util](Namespace.util.md) / chatInfoRef + +# Function: chatInfoRef() + +> **chatInfoRef**(`cInfo`): `ChatRef` \| `undefined` + +Defined in: [src/util.ts:4](../src/util.ts#L4) + +## Parameters + +### cInfo + +`ChatInfo` + +## Returns + +`ChatRef` \| `undefined` diff --git a/packages/simplex-chat-nodejs/docs/util.Function.ciBotCommand.md b/packages/simplex-chat-nodejs/docs/util.Function.ciBotCommand.md new file mode 100644 index 0000000000..bc7a66f163 --- /dev/null +++ b/packages/simplex-chat-nodejs/docs/util.Function.ciBotCommand.md @@ -0,0 +1,21 @@ +[**simplex-chat**](README.md) + +*** + +[simplex-chat](README.md) / [util](Namespace.util.md) / ciBotCommand + +# Function: ciBotCommand() + +> **ciBotCommand**(`chatItem`): [`BotCommand`](util.Interface.BotCommand.md) \| `undefined` + +Defined in: [src/util.ts:78](../src/util.ts#L78) + +## Parameters + +### chatItem + +`ChatItem` + +## Returns + +[`BotCommand`](util.Interface.BotCommand.md) \| `undefined` diff --git a/packages/simplex-chat-nodejs/docs/util.Function.ciContentText.md b/packages/simplex-chat-nodejs/docs/util.Function.ciContentText.md new file mode 100644 index 0000000000..7ab0bc540c --- /dev/null +++ b/packages/simplex-chat-nodejs/docs/util.Function.ciContentText.md @@ -0,0 +1,21 @@ +[**simplex-chat**](README.md) + +*** + +[simplex-chat](README.md) / [util](Namespace.util.md) / ciContentText + +# Function: ciContentText() + +> **ciContentText**(`__namedParameters`): `string` \| `undefined` + +Defined in: [src/util.ts:64](../src/util.ts#L64) + +## Parameters + +### \_\_namedParameters + +`ChatItem` + +## Returns + +`string` \| `undefined` diff --git a/packages/simplex-chat-nodejs/docs/util.Function.contactAddressStr.md b/packages/simplex-chat-nodejs/docs/util.Function.contactAddressStr.md new file mode 100644 index 0000000000..3f6c7b9562 --- /dev/null +++ b/packages/simplex-chat-nodejs/docs/util.Function.contactAddressStr.md @@ -0,0 +1,21 @@ +[**simplex-chat**](README.md) + +*** + +[simplex-chat](README.md) / [util](Namespace.util.md) / contactAddressStr + +# Function: contactAddressStr() + +> **contactAddressStr**(`link`): `string` + +Defined in: [src/util.ts:44](../src/util.ts#L44) + +## Parameters + +### link + +`CreatedConnLink` + +## Returns + +`string` diff --git a/packages/simplex-chat-nodejs/docs/util.Function.fromLocalProfile.md b/packages/simplex-chat-nodejs/docs/util.Function.fromLocalProfile.md new file mode 100644 index 0000000000..c32081b6cf --- /dev/null +++ b/packages/simplex-chat-nodejs/docs/util.Function.fromLocalProfile.md @@ -0,0 +1,21 @@ +[**simplex-chat**](README.md) + +*** + +[simplex-chat](README.md) / [util](Namespace.util.md) / fromLocalProfile + +# Function: fromLocalProfile() + +> **fromLocalProfile**(`__namedParameters`): `Profile` + +Defined in: [src/util.ts:56](../src/util.ts#L56) + +## Parameters + +### \_\_namedParameters + +`LocalProfile` + +## Returns + +`Profile` diff --git a/packages/simplex-chat-nodejs/docs/util.Function.reactionText.md b/packages/simplex-chat-nodejs/docs/util.Function.reactionText.md new file mode 100644 index 0000000000..896dc74a12 --- /dev/null +++ b/packages/simplex-chat-nodejs/docs/util.Function.reactionText.md @@ -0,0 +1,21 @@ +[**simplex-chat**](README.md) + +*** + +[simplex-chat](README.md) / [util](Namespace.util.md) / reactionText + +# Function: reactionText() + +> **reactionText**(`reaction`): `string` + +Defined in: [src/util.ts:89](../src/util.ts#L89) + +## Parameters + +### reaction + +`ACIReaction` + +## Returns + +`string` diff --git a/packages/simplex-chat-nodejs/docs/util.Function.senderName.md b/packages/simplex-chat-nodejs/docs/util.Function.senderName.md new file mode 100644 index 0000000000..6bacad97f6 --- /dev/null +++ b/packages/simplex-chat-nodejs/docs/util.Function.senderName.md @@ -0,0 +1,25 @@ +[**simplex-chat**](README.md) + +*** + +[simplex-chat](README.md) / [util](Namespace.util.md) / senderName + +# Function: senderName() + +> **senderName**(`cInfo`, `chatDir`): `string` + +Defined in: [src/util.ts:37](../src/util.ts#L37) + +## Parameters + +### cInfo + +`ChatInfo` + +### chatDir + +`CIDirection` + +## Returns + +`string` diff --git a/packages/simplex-chat-nodejs/docs/util.Interface.BotCommand.md b/packages/simplex-chat-nodejs/docs/util.Interface.BotCommand.md new file mode 100644 index 0000000000..21c3ad72ba --- /dev/null +++ b/packages/simplex-chat-nodejs/docs/util.Interface.BotCommand.md @@ -0,0 +1,25 @@ +[**simplex-chat**](README.md) + +*** + +[simplex-chat](README.md) / [util](Namespace.util.md) / BotCommand + +# Interface: BotCommand + +Defined in: [src/util.ts:72](../src/util.ts#L72) + +## Properties + +### keyword + +> **keyword**: `string` + +Defined in: [src/util.ts:73](../src/util.ts#L73) + +*** + +### params + +> **params**: `string` + +Defined in: [src/util.ts:74](../src/util.ts#L74) diff --git a/packages/simplex-chat-nodejs/examples/squaring-bot-readme.js b/packages/simplex-chat-nodejs/examples/squaring-bot-readme.js new file mode 100644 index 0000000000..f880808bcf --- /dev/null +++ b/packages/simplex-chat-nodejs/examples/squaring-bot-readme.js @@ -0,0 +1,17 @@ +(async () => { + const {bot} = await import("../dist/index.js") + const [chat, _user, _address] = await bot.run({ + profile: {displayName: "Squaring bot example", fullName: ""}, + dbOpts: {dbFilePrefix: "./squaring_bot", dbKey: ""}, + options: { + addressSettings: {welcomeMessage: "If you send me a number, I will calculate its square."}, + }, + onMessage: async (ci, content) => { + const n = +content.text + const reply = typeof n === "number" && !isNaN(n) + ? `${n} * ${n} = ${n * n}` + : `this is not a number` + await chat.apiSendTextReply(ci, reply) + } + }) +})() diff --git a/packages/simplex-chat-nodejs/examples/squaring-bot.ts b/packages/simplex-chat-nodejs/examples/squaring-bot.ts new file mode 100644 index 0000000000..682e7b887a --- /dev/null +++ b/packages/simplex-chat-nodejs/examples/squaring-bot.ts @@ -0,0 +1,42 @@ +import {T} from "@simplex-chat/types" +import {bot, util} from "../dist" + +(async () => { + const welcomeMessage = "Hello! I am a simple squaring bot.\n\nIf you send me a number, I will calculate its square." + const [chat, _user, _address] = await bot.run({ + profile: {displayName: "Squaring bot example", fullName: ""}, + dbOpts: {dbFilePrefix: "./squaring_bot", dbKey: ""}, + options: { + addressSettings: {autoAccept: true, welcomeMessage, businessAddress: false}, + commands: [ // commands to show in client UI + {type: "command", keyword: "help", label: "Send welcome message"}, + {type: "command", keyword: "info", label: "More information (not implemented)"} + ], + logContacts: true, + logNetwork: false + }, + onMessage: async (ci, content) => { + const n = +content.text + const reply = typeof n === "number" && !isNaN(n) + ? `${n} * ${n} = ${n * n}` + : `this is not a number` + await chat.apiSendTextReply(ci, reply) + }, + onCommands: { // command handlers can be different from commands to be shown in client UI + "help": async (ci: T.AChatItem, _cmd: util.BotCommand) => { + await chat.apiSendTextMessage(ci.chatInfo, welcomeMessage) + }, + // fallback handler that will be called for all other commands + "": async (ci: T.AChatItem, _cmd: util.BotCommand) => { + await chat.apiSendTextReply(ci, "This command is not supported") + } + }, + // If you use `onMessage` and subscribe to "newChatItems" event, exclude content messages from processing + // If you use `onCommands` and subscribe to "newChatItems" event, exclude commands from processing + events: { + "chatItemReaction": ({added, reaction}) => { + console.log(`${util.senderName(reaction.chatInfo, reaction.chatReaction.chatDir)} ${added ? "added" : "removed"} reaction ${util.reactionText(reaction)}`) + } + }, + }) +})() diff --git a/packages/simplex-chat-nodejs/jest.config.js b/packages/simplex-chat-nodejs/jest.config.js new file mode 100644 index 0000000000..18c5ddf0e5 --- /dev/null +++ b/packages/simplex-chat-nodejs/jest.config.js @@ -0,0 +1,10 @@ +module.exports = { + preset: "ts-jest", + maxWorkers: 1, + testEnvironment: "node", + transform: { + '^.+\\.ts$': ['ts-jest', { + tsconfig: 'tests/tsconfig.json' + }] + } +} diff --git a/packages/simplex-chat-nodejs/package.json b/packages/simplex-chat-nodejs/package.json new file mode 100644 index 0000000000..1ef1429e65 --- /dev/null +++ b/packages/simplex-chat-nodejs/package.json @@ -0,0 +1,58 @@ +{ + "name": "simplex-chat", + "version": "6.5.0-beta.4.2", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "files": [ + "src", + "cpp", + "dist", + "binding.gyp", + "tsconfig.json", + "docs", + "examples" + ], + "scripts": { + "preinstall": "node src/download-libs.js", + "install": "node-gyp configure; node-gyp rebuild --release", + "install-tools": "npm install -g node-gyp", + "configure": "node-gyp configure; mkdir libs 2> /dev/null | true", + "build": "node-gyp rebuild && tsc && cp ./src/simplex.* ./dist", + "run": "node src/index.js", + "build-run": "node-gyp build && node src/index.js", + "test": "jest", + "docs": "typedoc" + }, + "dependencies": { + "@simplex-chat/types": "^0.3.0", + "extract-zip": "^2.0.1", + "fast-deep-equal": "^3.1.3", + "node-addon-api": "^8.5.0" + }, + "devDependencies": { + "@types/jest": "^30.0.0", + "@types/node": "^25.0.5", + "jest": "^30.2.0", + "ts-jest": "^29.4.6", + "typedoc": "^0.28.15", + "typedoc-plugin-markdown": "^4.9.0", + "typescript": "^5.9.3" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/simplex-chat/simplex-chat.git" + }, + "keywords": [ + "messenger", + "chat", + "privacy", + "security" + ], + "author": "SimpleX Chat", + "license": "AGPL-3.0", + "bugs": { + "url": "https://github.com/simplex-chat/simplex-chat/issues" + }, + "homepage": "https://github.com/simplex-chat/simplex-chat/tree/stable/packages/simplex-chat-nodejs#readme", + "description": "SimpleX Chat Node.js library for chat bots" +} diff --git a/packages/simplex-chat-nodejs/src/api.ts b/packages/simplex-chat-nodejs/src/api.ts new file mode 100644 index 0000000000..dc87055f87 --- /dev/null +++ b/packages/simplex-chat-nodejs/src/api.ts @@ -0,0 +1,834 @@ +import {CC, CEvt, ChatEvent, ChatResponse, T} from "@simplex-chat/types" +import * as core from "./core" +import * as util from "./util" + +export class ChatCommandError extends Error { + constructor(public message: string, public response: ChatResponse) { + super(message) + } +} + +/** + * Connection request types. + * @enum {string} + */ +export enum ConnReqType { + Invitation = "invitation", + Contact = "contact", +} + +/** + * Bot address settings. + */ +export interface BotAddressSettings { + /** + * Automatically accept contact requests. + * @default true + */ + autoAccept?: boolean + + /** + * Optional welcome message to show before connection to the users. + * @default undefined (no welcome message) + */ + welcomeMessage?: T.MsgContent | string | undefined + + /** + * Business contact address. + * For all requests business chats will be created where other participants can be added. + * @default false + */ + businessAddress?: boolean +} + +export const defaultBotAddressSettings: BotAddressSettings = { + autoAccept: true, + welcomeMessage: undefined, + businessAddress: false +} + +export type EventSubscriberFunc = (event: ChatEvent & {type: K}) => void | Promise + +export type EventSubscribers = {[K in CEvt.Tag]?: EventSubscriberFunc} + +interface EventSubscriber { + subscriber: EventSubscriberFunc + once: boolean +} + +/** + * Main API class for interacting with the chat core library. + */ +export class ChatApi { + private receiveEvents = false + private eventsLoop: Promise | undefined = undefined + private subscribers: {[K in CEvt.Tag]?: EventSubscriber[]} = {} + private receivers: EventSubscriberFunc[] = [] + + private constructor(protected ctrl_: bigint | undefined) {} + + /** + * Initializes the ChatApi. + * @param {string} dbFilePrefix - File prefix for the database files. + * @param {string} [dbKey=""] - Database encryption key. + * @param {core.MigrationConfirmation} [confirm=core.MigrationConfirmation.YesUp] - Migration confirmation mode. + */ + static async init( + dbFilePrefix: string, + dbKey: string = "", + confirm = core.MigrationConfirmation.YesUp + ): Promise { + const ctrl = await core.chatMigrateInit(dbFilePrefix, dbKey, confirm) + return new ChatApi(ctrl) + } + + /** + * Start chat controller. Must be called with the existing user profile. + */ + async startChat(): Promise { + this.receiveEvents = true + this.eventsLoop = this.runEventsLoop() + const r = await this.sendChatCmd(CC.StartChat.cmdString({mainApp: true, enableSndFiles: true})) + if (r.type !== "chatStarted" && r.type !== "chatRunning") { + throw new ChatCommandError("error starting chat", r) + } + } + + /** + * Stop chat controller. + * Must be called before closing the database. + * Usually doesn't need to be called in chat bots. + */ + async stopChat(): Promise { + const r = await this.sendChatCmd("/_stop") + if (r.type !== "chatStopped") throw new ChatCommandError("error starting chat", r) + this.receiveEvents = false + if (this.eventsLoop) await this.eventsLoop + this.eventsLoop = undefined + } + + /** + * Close chat database. + * Usually doesn't need to be called in chat bots. + */ + async close(): Promise { + this.receiveEvents = false + if (this.eventsLoop) await this.eventsLoop + this.eventsLoop = undefined + await core.chatCloseStore(this.ctrl) + this.ctrl_ = undefined + } + + private async runEventsLoop(): Promise { + while (this.receiveEvents) { + try { + const event = await this.recvChatEvent() + if (!event) continue + const subs = this.subscribers[event.type] + if (subs) { + for (const {subscriber, once} of [...subs]) { + try { + const p = (subscriber as EventSubscriberFunc)(event) + if (p instanceof Promise) await p + } catch(e) { + console.log(`${event.type} event processing error`, e) + } + if (once) this.off(event.type, subscriber as EventSubscriberFunc) + } + } + for (const r of [...this.receivers]) { + try { + const p = r(event) + if (p instanceof Promise) await p + } catch(e) { + console.log(`${event.type} event processing error`, e) + } + } + } catch(err) { + const e = err as core.ChatAPIError + if ("chatError" in e) { + console.log("Chat error", e.chatError) + } else { + console.log("Invalid event", e) + } + } + } + } + + /** + * Subscribe multiple event handlers at once. + * @param subscribers - An object mapping event types (CEvt.Tag) to their subscriber functions. + * @throws {Error} If the same function is subscribed to event. + */ + on(subscribers: EventSubscribers): void + + /** + * Subscribe a handler to a specific event. + * @param {CEvt.Tag} event - The event type to subscribe to. + * @param subscriber - The subscriber function for the event. + * @throws {Error} If the same function is subscribed to event. + */ + on(event: K, subscriber: EventSubscriberFunc): void + on(events: K | EventSubscribers, subscriber?: EventSubscriberFunc): void { + if (typeof events === "string" && subscriber) { + this.on_(events, subscriber) + } else { + const eventEntries = Object.entries(events) as [CEvt.Tag, EventSubscriberFunc | undefined][] + for (const [event, subscriber] of eventEntries) { + if (subscriber) this.on_(event, subscriber) + } + } + } + + private on_(event: K, subscriber: EventSubscriberFunc, once: boolean = false): void { + const subs: EventSubscriber[] = this.subscribers[event] || (this.subscribers[event] = []) + if (subs.some(s => s.subscriber === subscriber)) throw Error(`this function is already subscribed to ${event}`) + subs.push({subscriber, once}) + } + + /** + * Subscribe a handler to any event. + * @param receiver - The receiver function for any event. + * @throws {Error} If the same function is subscribed to event. + */ + onAny(receiver: EventSubscriberFunc): void { + if (this.receivers.some(s => s === receiver)) throw Error("this function is already subscribed") + this.receivers.push(receiver) + } + + /** + * Subscribe a handler to a specific event to be delivered one time. + * @param {CEvt.Tag} event - The event type to subscribe to. + * @param subscriber - The subscriber function for the event. + * @throws {Error} If the same function is subscribed to event. + */ + once(event: K, subscriber: EventSubscriberFunc): void { + this.on_(event, subscriber, true) + } + + /** + * Waits for specific event, with an optional predicate. + * Returns `undefined` on timeout if specified. + */ + wait(event: K): Promise + wait(event: K, predicate: ((event: ChatEvent & {type: K}) => boolean) | undefined): Promise + wait(event: K, timeout: number): Promise + wait(event: K, predicate: ((event: ChatEvent & {type: K}) => boolean) | undefined, timeout: number): Promise + wait( + event: K, + predicate: ((event: ChatEvent & {type: K}) => boolean) | undefined | number = undefined, // number for timeout + timeout: number = 0 // milliseconds, default - indefinite + ): Promise { + if (typeof predicate === "number") { + timeout = predicate + predicate = undefined + } + return new Promise((resolve, reject) => { + let done = false + const cleanup = () => { + done = true + this.off(event, subscriber) + } + const subscriber: EventSubscriberFunc = async (evt: ChatEvent & {type: K}) => { + if (done) return + if (predicate) { + try { if (!predicate(evt)) return } + catch(e) { cleanup(); reject(e); return } + } + cleanup() + resolve(evt) + } + this.on(event, subscriber) + if (timeout > 0) { + setTimeout(() => { if (!done) { cleanup(); resolve(undefined) } }, timeout) + } + }) + } + + /** + * Unsubscribe all or a specific handler from a specific event. + * @param {CEvt.Tag} event - The event type to unsubscribe from. + * @param subscriber - An optional subscriber function for the event. + */ + off(event: K, subscriber: EventSubscriberFunc | undefined = undefined): void { + if (subscriber) { + const subs = this.subscribers[event] + if (subs) { + const i = subs.findIndex(s => s.subscriber === subscriber) + if (i >= 0) subs.splice(i, 1) + } + } else { + delete this.subscribers[event] + } + } + + /** + * Unsubscribe all or a specific handler from any events. + * @param receiver - An optional subscriber function for the event. + */ + offAny(receiver: EventSubscriberFunc | undefined = undefined): void { + if (receiver) { + const i = this.receivers.findIndex(r => r === receiver) + if (i >= 0) this.receivers.splice(i, 1) + } else { + this.receivers = [] + } + } + + /** + * Chat controller is initialized + */ + get initialized(): boolean { + return typeof this.ctrl_ === "bigint" + } + + /** + * Chat controller is started + */ + get started(): boolean { + return this.receiveEvents && this.eventsLoop !== undefined + } + + /** + * Chat controller reference + */ + get ctrl(): bigint { + if (typeof this.ctrl_ === "bigint") return this.ctrl_ + else throw Error("chat api controller not initialized") + } + + async sendChatCmd(cmd: string): Promise { + return await core.chatSendCmd(this.ctrl, cmd) + } + + async recvChatEvent(wait: number = 5_000_000): Promise { + return await core.chatRecvMsgWait(this.ctrl, wait) + } + + /** + * Create bot address. + * Network usage: interactive. + */ + async apiCreateUserAddress(userId: number): Promise { + const r = await this.sendChatCmd(CC.APICreateMyAddress.cmdString({userId})) + if (r.type === "userContactLinkCreated") return r.connLinkContact + throw new ChatCommandError("error creating user address", r) + } + + /** + * Deletes a user address. + * Network usage: background. + */ + async apiDeleteUserAddress(userId: number): Promise { + const r = await this.sendChatCmd(CC.APIDeleteMyAddress.cmdString({userId})) + if (r.type === "userContactLinkDeleted") return + throw new ChatCommandError("error deleting user address", r) + } + + /** + * Get bot address and settings. + * Network usage: no. + */ + async apiGetUserAddress(userId: number): Promise { + try { + const r = await this.sendChatCmd(CC.APIShowMyAddress.cmdString({userId})) + switch (r.type) { + case "userContactLink": return r.contactLink + default: throw new ChatCommandError("error loading user address", r) + } + } catch (err) { + const e = err as any + if (e.chatError?.type === "errorStore" && e.chatError.storeError?.type === "userContactLinkNotFound") return undefined + throw e + } + } + + /** + * Add address to bot profile. + * Network usage: interactive. + */ + async apiSetProfileAddress(userId: number, enable: boolean): Promise { + const r = await this.sendChatCmd(CC.APISetProfileAddress.cmdString({userId, enable})) + switch (r.type) { + case "userProfileUpdated": + return r.updateSummary + default: + throw new ChatCommandError("error loading user address", r) + } + } + + /** + * Set bot address settings. + * Network usage: interactive. + */ + async apiSetAddressSettings(userId: number, {autoAccept, welcomeMessage, businessAddress}: BotAddressSettings): Promise { + const autoReply = welcomeMessage || defaultBotAddressSettings.welcomeMessage + const settings: T.AddressSettings = { + autoAccept: (autoAccept === undefined ? defaultBotAddressSettings.autoAccept : autoAccept) ? {acceptIncognito: false} : undefined, + autoReply: typeof autoReply === "string" ? {type: "text", text: autoReply} : autoReply, + businessAddress: businessAddress || defaultBotAddressSettings.businessAddress || false + } + const r = await this.sendChatCmd(CC.APISetAddressSettings.cmdString({userId, settings})) + if (r.type !== "userContactLinkUpdated") { + throw new ChatCommandError("error changing user contact address settings", r) + } + } + + /** + * Send messages. + * Network usage: background. + */ + async apiSendMessages(chat: [T.ChatType, number] | T.ChatRef | T.ChatInfo, messages: T.ComposedMessage[], liveMessage = false): Promise { + const sendRef = Array.isArray(chat) + ? {chatType: chat[0], chatId: chat[1]} + : "chatType" in chat + ? chat + : util.chatInfoRef(chat) + if (!sendRef) throw Error("apiSendMessages: can't send messages to this chat") + const r = await this.sendChatCmd( + CC.APISendMessages.cmdString({ + sendRef, + composedMessages: messages, + liveMessage + }) + ) + if (r.type === "newChatItems") return r.chatItems + throw new ChatCommandError("unexpected response", r) + } + + /** + * Send text message. + * Network usage: background. + */ + async apiSendTextMessage(chat: [T.ChatType, number] | T.ChatRef | T.ChatInfo, text: string, inReplyTo?: number): Promise { + return this.apiSendMessages(chat, [{msgContent: {type: "text", text}, mentions: {}, quotedItemId: inReplyTo}]) + } + + /** + * Send text message in reply to received message. + * Network usage: background. + */ + async apiSendTextReply(chatItem: T.AChatItem, text: string): Promise { + return this.apiSendTextMessage(chatItem.chatInfo, text, chatItem.chatItem.meta.itemId) + } + + /** + * Update message. + * Network usage: background. + */ + async apiUpdateChatItem(chatType: T.ChatType, chatId: number, chatItemId: number, msgContent: T.MsgContent, liveMessage: false): Promise { + const r = await this.sendChatCmd( + CC.APIUpdateChatItem.cmdString({ + chatRef: {chatType, chatId}, + chatItemId, + liveMessage, + updatedMessage: {msgContent, mentions: {}}, + }) + ) + if (r.type === "chatItemUpdated") return r.chatItem.chatItem + throw new ChatCommandError("error updating chat item", r) + } + + /** + * Delete message. + * Network usage: background. + */ + async apiDeleteChatItems( + chatType: T.ChatType, + chatId: number, + chatItemIds: number[], + deleteMode: T.CIDeleteMode + ): Promise { + const r = await this.sendChatCmd(CC.APIDeleteChatItem.cmdString({chatRef: {chatType, chatId}, chatItemIds, deleteMode})) + if (r.type === "chatItemsDeleted") return r.chatItemDeletions + throw new ChatCommandError("error deleting chat item", r) + } + + /** + * Moderate message. Requires Moderator role (and higher than message author's). + * Network usage: background. + */ + async apiDeleteMemberChatItem(groupId: number, chatItemIds: number[]): Promise { + const r = await this.sendChatCmd(CC.APIDeleteMemberChatItem.cmdString({groupId, chatItemIds})) + if (r.type === "chatItemsDeleted") return r.chatItemDeletions + throw new ChatCommandError("error deleting member chat item", r) + } + + /** + * Add/remove message reaction. + * Network usage: background. + */ + async apiChatItemReaction( + chatType: T.ChatType, + chatId: number, + chatItemId: number, + add: boolean, + reaction: T.MsgReaction + ) { + const r = await this.sendChatCmd(CC.APIChatItemReaction.cmdString({chatRef: {chatType, chatId}, chatItemId, add, reaction})) + if (r.type === "chatItemsDeleted") return r.chatItemDeletions + throw new ChatCommandError("error setting item reaction", r) + } + + /** + * Receive file. + * Network usage: no. + */ + async apiReceiveFile(fileId: number): Promise { + const r = await this.sendChatCmd(CC.ReceiveFile.cmdString({fileId, userApprovedRelays: true})) + if (r.type === "rcvFileAccepted") return r.chatItem + throw new ChatCommandError("error receiving file", r) + } + + /** + * Cancel file. + * Network usage: background. + */ + async apiCancelFile(fileId: number): Promise { + const r = await this.sendChatCmd(CC.CancelFile.cmdString({fileId})) + if (r.type === "sndFileCancelled" || r.type === "rcvFileCancelled") return + throw new ChatCommandError("error canceling file", r) + } + + /** + * Add contact to group. Requires bot to have Admin role. + * Network usage: interactive. + */ + async apiAddMember(groupId: number, contactId: number, memberRole: T.GroupMemberRole): Promise { + const r = await this.sendChatCmd(CC.APIAddMember.cmdString({groupId, contactId, memberRole})) + if (r.type === "sentGroupInvitation") return r.member + throw new ChatCommandError("error adding member", r) + } + + /** + * Join group. + * Network usage: interactive. + */ + async apiJoinGroup(groupId: number): Promise { + const r = await this.sendChatCmd(CC.APIJoinGroup.cmdString({groupId})) + if (r.type === "userAcceptedGroupSent") return r.groupInfo + throw new ChatCommandError("error joining group", r) + } + + /** + * Accept group member. Requires Admin role. + * Network usage: background. + */ + async apiAcceptMember(groupId: number, groupMemberId: number, memberRole: T.GroupMemberRole): Promise { + const r = await this.sendChatCmd(CC.APIAcceptMember.cmdString({groupId, groupMemberId, memberRole})) + if (r.type === "memberAccepted") return r.member + throw new ChatCommandError("error accepting member", r) + } + + /** + * Set members role. Requires Admin role. + * Network usage: background. + */ + async apiSetMembersRole(groupId: number, groupMemberIds: number[], memberRole: T.GroupMemberRole): Promise { + const r = await this.sendChatCmd(CC.APIMembersRole.cmdString({groupId, groupMemberIds, memberRole})) + if (r.type === "membersRoleUser") return + throw new ChatCommandError("error setting members role", r) + } + + /** + * Block members. Requires Moderator role. + * Network usage: background. + */ + async apiBlockMembersForAll(groupId: number, groupMemberIds: number[], blocked: boolean): Promise { + const r = await this.sendChatCmd(CC.APIBlockMembersForAll.cmdString({groupId, groupMemberIds, blocked})) + if (r.type === "membersBlockedForAllUser") return + throw new ChatCommandError("error blocking members", r) + } + + /** + * Remove members. Requires Admin role. + * Network usage: background. + */ + async apiRemoveMembers(groupId: number, memberIds: number[], withMessages = false): Promise { + const r = await this.sendChatCmd(CC.APIRemoveMembers.cmdString({groupId, groupMemberIds: memberIds, withMessages})) + if (r.type === "userDeletedMembers") return r.members + throw new ChatCommandError("error removing member", r) + } + + /** + * Leave group. + * Network usage: background. + */ + async apiLeaveGroup(groupId: number): Promise { + const r = await this.sendChatCmd(CC.APILeaveGroup.cmdString({groupId})) + if (r.type === "leftMemberUser") return r.groupInfo + throw new ChatCommandError("error leaving group", r) + } + + /** + * Get group members. + * Network usage: no. + */ + async apiListMembers(groupId: number): Promise { + const r = await this.sendChatCmd(CC.APIListMembers.cmdString({groupId})) + if (r.type === "groupMembers") return r.group.members + throw new ChatCommandError("error getting group members", r) + } + + /** + * Create group. + * Network usage: no. + */ + async apiNewGroup(userId: number, groupProfile: T.GroupProfile): Promise { + const r = await this.sendChatCmd(CC.APINewGroup.cmdString({userId, groupProfile, incognito: false})) + if (r.type === "groupCreated") return r.groupInfo + throw new ChatCommandError("error creating group", r) + } + + /** + * Update group profile. + * Network usage: background. + */ + async apiUpdateGroupProfile(groupId: number, groupProfile: T.GroupProfile): Promise { + const r = await this.sendChatCmd(CC.APIUpdateGroupProfile.cmdString({groupId, groupProfile})) + if (r.type === "groupUpdated") return r.toGroup + throw new ChatCommandError("error updating group", r) + } + + /** + * Create group link. + * Network usage: interactive. + */ + async apiCreateGroupLink(groupId: number, memberRole: T.GroupMemberRole): Promise { + const r = await this.sendChatCmd(CC.APICreateGroupLink.cmdString({groupId, memberRole})) + if (r.type === "groupLinkCreated") { + const link = r.groupLink.connLinkContact + return link.connShortLink || link.connFullLink + } + throw new ChatCommandError("error creating group link", r) + } + + /** + * Set member role for group link. + * Network usage: no. + */ + async apiSetGroupLinkMemberRole(groupId: number, memberRole: T.GroupMemberRole): Promise { + const r = await this.sendChatCmd(CC.APIGroupLinkMemberRole.cmdString({groupId, memberRole})) + if (r.type !== "groupLink") throw new ChatCommandError("error setting group link member role", r) + } + + /** + * Delete group link. + * Network usage: background. + */ + async apiDeleteGroupLink(groupId: number): Promise { + const r = await this.sendChatCmd(CC.APIDeleteGroupLink.cmdString({groupId})) + if (r.type !== "groupLinkDeleted") throw new ChatCommandError("error deleting group link", r) + } + + /** + * Get group link. + * Network usage: no. + */ + async apiGetGroupLink(groupId: number): Promise { + const r = await this.sendChatCmd(CC.APIGetGroupLink.cmdString({groupId})) + if (r.type === "groupLink") return r.groupLink + throw new ChatCommandError("error getting group link", r) + } + + async apiGetGroupLinkStr(groupId: number): Promise { + const link = (await this.apiGetGroupLink(groupId)).connLinkContact + return link.connShortLink || link.connFullLink + } + + /** + * Create 1-time invitation link. + * Network usage: interactive. + */ + async apiCreateLink(userId: number): Promise { + const r = await this.sendChatCmd(CC.APIAddContact.cmdString({userId, incognito: false})) + if (r.type === "invitation") { + const link = r.connLinkInvitation + return link.connShortLink || link.connFullLink + } + throw new ChatCommandError("error creating link", r) + } + + /** + * Determine SimpleX link type and if the bot is already connected via this link. + * Network usage: interactive. + */ + async apiConnectPlan(userId: number, connectionLink: string): Promise<[T.ConnectionPlan, T.CreatedConnLink]> { + const r = await this.sendChatCmd(CC.APIConnectPlan.cmdString({userId, connectionLink})) + if (r.type === "connectionPlan") return [r.connectionPlan, r.connLink] + throw new ChatCommandError("error getting connect plan", r) + } + + /** + * Connect via prepared SimpleX link. The link can be 1-time invitation link, contact address or group link + * Network usage: interactive. + */ + async apiConnect(userId: number, incognito: boolean, preparedLink?: T.CreatedConnLink): Promise { + const r = await this.sendChatCmd(CC.APIConnect.cmdString({userId, incognito, preparedLink_: preparedLink})) + return this.handleConnectResult(r) + } + + /** + * Connect via SimpleX link as string in the active user profile. + * Network usage: interactive. + */ + async apiConnectActiveUser(connLink: string): Promise { + const r = await this.sendChatCmd(CC.Connect.cmdString({incognito: false, connLink_: connLink})) + return this.handleConnectResult(r) + } + + private handleConnectResult(r: ChatResponse): ConnReqType { + switch (r.type) { + case "sentConfirmation": + return ConnReqType.Invitation + case "sentInvitation": + return ConnReqType.Contact + case "contactAlreadyExists": + throw new ChatCommandError("contact already exists", r) + default: + throw new ChatCommandError("connection error", r) + } + } + + /** + * Accept contact request. + * Network usage: interactive. + */ + async apiAcceptContactRequest(contactReqId: number): Promise { + const r = await this.sendChatCmd(CC.APIAcceptContact.cmdString({contactReqId})) + if (r.type === "acceptingContactRequest") return r.contact + throw new ChatCommandError("error accepting contact request", r) + } + + /** + * Reject contact request. The user who sent the request is **not notified**. + * Network usage: no. + */ + async apiRejectContactRequest(contactReqId: number): Promise { + const r = await this.sendChatCmd(CC.APIRejectContact.cmdString({contactReqId})) + if (r.type === "contactRequestRejected") return + throw new ChatCommandError("error rejecting contact request", r) + } + + /** + * Get contacts. + * Network usage: no. + */ + async apiListContacts(userId: number): Promise { + const r = await this.sendChatCmd(CC.APIListContacts.cmdString({userId})) + if (r.type === "contactsList") return r.contacts + throw new ChatCommandError("error listing contacts", r) + } + + /** + * Get groups. + * Network usage: no. + */ + async apiListGroups(userId: number, contactId?: number, search?: string): Promise { + const r = await this.sendChatCmd(CC.APIListGroups.cmdString({userId, contactId_: contactId, search})) + if (r.type === "groupsList") return r.groups + throw new ChatCommandError("error listing groups", r) + } + + /** + * Delete chat. + * Network usage: background. + */ + async apiDeleteChat(chatType: T.ChatType, chatId: number, deleteMode: T.ChatDeleteMode = {type: "full", notify: true}): Promise { + const r = await this.sendChatCmd(CC.APIDeleteChat.cmdString({chatRef: {chatType, chatId}, chatDeleteMode: deleteMode})) + switch (chatType) { + case T.ChatType.Direct: + if (r.type === "contactDeleted") return + break + case T.ChatType.Group: + if (r.type === "groupDeletedUser") return + break + } + throw new ChatCommandError("error deleting chat", r) + } + + /** + * Get active user profile + * Network usage: no. + */ + async apiGetActiveUser(): Promise { + try { + const r = await this.sendChatCmd(CC.ShowActiveUser.cmdString({})) + switch (r.type) { + case "activeUser": + return r.user + default: + throw new ChatCommandError("unexpected response", r) + } + } catch (err) { + const e = err as core.ChatAPIError + if (e.chatError?.type === "error" && e.chatError.errorType.type === "noActiveUser") return undefined + throw err + } + } + + /** + * Create new user profile + * Network usage: no. + */ + async apiCreateActiveUser(profile?: T.Profile): Promise { + const r = await this.sendChatCmd(CC.CreateActiveUser.cmdString({newUser: {profile, pastTimestamp: false}})) + if (r.type === "activeUser") return r.user + throw new ChatCommandError("unexpected response", r) + } + + /** + * Get all user profiles + * Network usage: no. + */ + async apiListUsers(): Promise { + const r = await this.sendChatCmd(CC.ListUsers.cmdString({})) + if (r.type === "usersList") return r.users + throw new ChatCommandError("error listing users", r) + } + + /** + * Set active user profile + * Network usage: no. + */ + async apiSetActiveUser(userId: number, viewPwd?: string): Promise { + const r = await this.sendChatCmd(CC.APISetActiveUser.cmdString({userId, viewPwd})) + if (r.type === "activeUser") return r.user + throw new ChatCommandError("error setting active user", r) + } + + /** + * Delete user profile. + * Network usage: background. + */ + async apiDeleteUser(userId: number, delSMPQueues: boolean, viewPwd?: string): Promise { + const r = await this.sendChatCmd(CC.APIDeleteUser.cmdString({userId, delSMPQueues, viewPwd})) + if (r.type === "cmdOk") return + throw new ChatCommandError("error deleting user", r) + } + + /** + * Update user profile. + * Network usage: background. + */ + async apiUpdateProfile(userId: number, profile: T.Profile): Promise { + const r = await this.sendChatCmd(CC.APIUpdateProfile.cmdString({userId, profile})) + switch (r.type) { + case "userProfileNoChange": + return undefined + case "userProfileUpdated": + return r.updateSummary + default: + throw new ChatCommandError("error updating profile", r) + } + } + + /** + * Configure chat preference overrides for the contact. + * Network usage: background. + */ + async apiSetContactPrefs(contactId: number, preferences: T.Preferences): Promise { + const r = await this.sendChatCmd(CC.APISetContactPrefs.cmdString({contactId, preferences})) + if (r.type !== "contactPrefsUpdated") throw new ChatCommandError("error setting contact prefs", r) + } +} diff --git a/packages/simplex-chat-nodejs/src/bot.ts b/packages/simplex-chat-nodejs/src/bot.ts new file mode 100644 index 0000000000..95a0c13d96 --- /dev/null +++ b/packages/simplex-chat-nodejs/src/bot.ts @@ -0,0 +1,216 @@ +import {T} from "@simplex-chat/types" +import * as api from "./api" +import * as core from "./core" +import * as util from "./util" +import equal = require("fast-deep-equal") + +export interface BotDbOpts { + dbFilePrefix: string // two schema files will be named _chat.db and _agent.db + dbKey?: string + confirmMigrations?: core.MigrationConfirmation +} + +export interface BotOptions { + createAddress?: boolean + updateAddress?: boolean + updateProfile?: boolean + addressSettings?: api.BotAddressSettings + allowFiles?: boolean + commands?: T.ChatBotCommand[] // commands to show in client UI + useBotProfile?: boolean // create profile not marked as a bot, with default preferences + logContacts?: boolean + logNetwork?: boolean +} + +const defaultOpts: Required = { + createAddress: true, + updateAddress: true, + updateProfile: true, + addressSettings: api.defaultBotAddressSettings, + allowFiles: false, + commands: [], + useBotProfile: true, + logContacts: true, + logNetwork: false +} + +export interface BotConfig { + profile: T.Profile, + dbOpts: BotDbOpts, + options: BotOptions, + onMessage?: (chatItem: T.AChatItem, content: T.MsgContent) => void | Promise, + // command handlers can be different from commands to be shown in client UI + onCommands?: {[K in string]?: ((chatItem: T.AChatItem, command: util.BotCommand) => void | Promise)}, + // If you use `onMessage` and to subscribe "newChatItems" event, exclude content messages from processing + // If you use `onCommands` and to subscribe "newChatItems" event, exclude commands from processing + events?: api.EventSubscribers +} + +export async function run({profile, dbOpts, options = defaultOpts, onMessage, onCommands = {}, events = {}}: BotConfig): Promise<[api.ChatApi, T.User, T.UserContactLink | undefined]> { + const bot = await api.ChatApi.init(dbOpts.dbFilePrefix, dbOpts.dbKey || "", dbOpts.confirmMigrations || core.MigrationConfirmation.YesUp) + const opts = fullOptions(options) + if (onMessage) subscribeMessages(bot, onMessage) + if (Object.keys(onCommands).length > 0) subscribeCommands(bot, onCommands) + if (Object.keys(events).length > 0) bot.on(events) + subscribeLogEvents(bot, opts) + const botProfile = mkBotProfile(profile, opts) + const user = await createBotUser(bot, botProfile) + await bot.startChat() + const address = await createOrUpdateAddress(bot, user, opts) + if (address) { + const addressLink = util.contactAddressStr(address.connLinkContact) + console.log(`Bot address: ${addressLink}`) + if (opts.useBotProfile) botProfile.contactLink = addressLink + } + await updateBotUserProfile(bot, user, botProfile, opts) + return [bot, user, address] +} + +function fullOptions(options: BotOptions): Required { + const opts = { + createAddress: options.createAddress ?? defaultOpts.createAddress, + updateAddress: options.updateAddress ?? defaultOpts.updateAddress, + updateProfile: options.updateProfile ?? defaultOpts.updateProfile, + addressSettings: options.addressSettings ?? defaultOpts.addressSettings, + allowFiles: options.allowFiles ?? defaultOpts.allowFiles, + commands: options.commands ?? defaultOpts.commands, + useBotProfile: options.useBotProfile ?? defaultOpts.useBotProfile, + logContacts: options.logContacts ?? defaultOpts.logContacts, + logNetwork: options.logNetwork ?? defaultOpts.logNetwork + } + const welcomeMessage = opts.addressSettings.welcomeMessage ?? defaultOpts.addressSettings.welcomeMessage + opts.addressSettings = { + autoAccept: opts.addressSettings.autoAccept ?? defaultOpts.addressSettings.autoAccept, + welcomeMessage: typeof welcomeMessage === "string" ? {type: "text", text: welcomeMessage} : welcomeMessage, + businessAddress: opts.addressSettings.businessAddress ?? defaultOpts.addressSettings.businessAddress + } + return opts +} + +function mkBotProfile(profile: T.Profile, opts: Required): T.Profile { + if (opts.useBotProfile) { + const prefs = profile.preferences || {} + if (prefs.files || prefs.calls || prefs.voice || prefs.commands) { + console.log("Option useBotProfile is enabled and profile preferences used for files, calls, voice or commands, exiting") + process.exit() + } + prefs.files = {allow: opts.allowFiles ? T.FeatureAllowed.Yes : T.FeatureAllowed.No} + prefs.calls = {allow: T.FeatureAllowed.No} + prefs.voice = {allow: T.FeatureAllowed.No} + prefs.commands = opts.commands + profile.preferences = prefs + profile.peerType = T.ChatPeerType.Bot + } else if (opts.commands.length > 0) { + console.log("Option useBotProfile is disabled and commands are passed, exiting") + process.exit() + } + return profile +} + +function subscribeMessages(bot: api.ChatApi, onMessage: (chatItem: T.AChatItem, content: T.MsgContent) => void | Promise) { + bot.on("newChatItems", async ({chatItems}) => { + for (const ci of chatItems) { + if (ci.chatItem.content.type === "rcvMsgContent") { + try { + const p = onMessage(ci, ci.chatItem.content.msgContent) + if (p instanceof Promise) await p + } catch (e) { + console.log("message processing error", e) + } + } + } + }) +} + +function subscribeCommands(bot: api.ChatApi, commands: {[K in string]?: ((chatItem: T.AChatItem, command: util.BotCommand) => void | Promise)}) { + bot.on("newChatItems", async (evt) => { + for (const ci of evt.chatItems) { + const cmd = util.ciBotCommand(ci.chatItem) + if (cmd) { + const cmdFunc = commands[cmd.keyword] || commands[""] + if (cmdFunc) { + try { + const p = cmdFunc(ci, cmd) + if (p instanceof Promise) await p + } catch(e) { + console.log(`${cmd} command processing error`, e) + } + } + } + } + }) +} + +function subscribeLogEvents(bot: api.ChatApi, opts: Required) { + if (opts.logContacts) { + bot.on({ + "contactConnected": ({contact}) => console.log(`${contact.profile.displayName} connected`), + "contactDeletedByContact": ({contact}) => console.log(`${contact.profile.displayName} deleted connection with bot`) + }) + } + if (opts.logNetwork) { + bot.on({ + "hostConnected": ({transportHost}) => console.log(`connected server ${transportHost}`), + "hostDisconnected": ({transportHost}) => console.log(`diconnected server ${transportHost}`), + "subscriptionStatus": ({subscriptionStatus, connections}) => console.log(`${connections.length} subscription(s) ${subscriptionStatus.type}`) + }) + } +} + +async function createBotUser(bot: api.ChatApi, profile: T.Profile): Promise { + let user = await bot.apiGetActiveUser() + if (!user) { + console.log("No active user in database, creating...") + user = await bot.apiCreateActiveUser(profile) + } + console.log("Bot user: ", user.profile.displayName) + return user +} + +async function createOrUpdateAddress(bot: api.ChatApi, user: T.User, opts: Required): Promise { + const {userId} = user + let address = await bot.apiGetUserAddress(userId) + if (!address) { + if (opts.createAddress) { + console.log("Bot has no address, creating...") + await bot.apiCreateUserAddress(userId) + address = await bot.apiGetUserAddress(userId) + if (!address) { + console.log("Failed reading created user address, exiting") + process.exit() + } + } else { + console.log("Warning: bot has no address") + return + } + } + + const addressSettings = opts.addressSettings || defaultOpts.addressSettings + if (!equal(util.botAddressSettings(address), addressSettings)) { + if (opts.updateAddress) { + console.log("Bot address settings changed, updating...") + await bot.apiSetAddressSettings(userId, addressSettings) + } else { + console.log("Bot address settings changed") + } + } + + return address +} + +async function updateBotUserProfile(bot: api.ChatApi, user: T.User, profile: T.Profile, opts: Required): Promise { + const {userId} = user + if (!equal(util.fromLocalProfile(user.profile), profile)) { + if (opts.updateProfile) { + console.log("Bot profile changed, updating...") + const summary = await bot.apiUpdateProfile(userId, profile) + console.log( + summary + ? `Bot profile updated: ${summary.updateSuccesses} updated contact(s), ${summary.updateFailures} failed contact update(s).` + : "Unexpected: profile did not change!" + ) + } else { + console.log("Bot profile changed") + } + } +} diff --git a/packages/simplex-chat-nodejs/src/core.ts b/packages/simplex-chat-nodejs/src/core.ts new file mode 100644 index 0000000000..949c2356af --- /dev/null +++ b/packages/simplex-chat-nodejs/src/core.ts @@ -0,0 +1,210 @@ +import {ChatEvent, ChatResponse, T} from "@simplex-chat/types" +import * as simplex from "./simplex" + +/** + * Initialize chat controller + */ +export async function chatMigrateInit(dbPath: string, dbKey: string, confirm: MigrationConfirmation): Promise { + const [ctrl, res] = await simplex.chat_migrate_init(dbPath, dbKey, confirm) + const json = JSON.parse(res) + if (json.type === 'ok') return ctrl + throw new ChatInitError("Database or migration error (see dbMigrationError property)", json as DBMigrationError) +} + +/** + * Close chat store + */ +export async function chatCloseStore(ctrl: bigint): Promise { + const res = await simplex.chat_close_store(ctrl) + if (res !== "") throw new Error(res) +} + +/** + * Send chat command as string + */ +export async function chatSendCmd(ctrl: bigint, cmd: string): Promise { + const res = await simplex.chat_send_cmd(ctrl, cmd) + const json = JSON.parse(res) as APIResult + // console.log(cmd.slice(0, 16), json.result?.type || json.error) + if (typeof json.result === 'object') return json.result + if (typeof json.error === 'object') throw new ChatAPIError("Chat command error (see chatError property)", json.error as T.ChatError) + throw new ChatAPIError("Invalid chat command result") +} + +/** + * Receive chat event + */ +export async function chatRecvMsgWait(ctrl: bigint, wait: number): Promise { + const res = await simplex.chat_recv_msg_wait(ctrl, wait) + if (res === "") return undefined + const json = JSON.parse(res) as APIResult + // if (json.result) console.log("event", json.result.type) + if (typeof json.result === 'object') return json.result + if (typeof json.error === 'object') throw new ChatAPIError("Chat event error (see chatError property)", json.error as T.ChatError) + throw new ChatAPIError("Invalid chat event") +} + +/** + * Write buffer to encrypted file + */ +export async function chatWriteFile(ctrl: bigint, path: string, buffer: ArrayBuffer): Promise { + const res = await simplex.chat_write_file(ctrl, path, buffer) + return cryptoArgsResult(res) +} + +/** + * Read buffer from encrypted file + */ +export async function chatReadFile(path: string, {fileKey, fileNonce}: CryptoArgs): Promise { + return await simplex.chat_read_file(path, fileKey, fileNonce) +} + +/** + * Encrypt file + */ +export async function chatEncryptFile(ctrl: bigint, fromPath: string, toPath: string): Promise { + const res = await simplex.chat_encrypt_file(ctrl, fromPath, toPath) + return cryptoArgsResult(res) +} + +/** + * Decrypt file + */ +export async function chatDecryptFile(fromPath: string, {fileKey, fileNonce}: CryptoArgs, toPath: string): Promise { + const res = await simplex.chat_decrypt_file(fromPath, fileKey, fileNonce, toPath) + if (res !== "") throw new Error(res) +} + +function cryptoArgsResult(res: string): CryptoArgs { + const json = JSON.parse(res) + switch (json.type) { + case "result": return json.cryptoArgs as CryptoArgs + case "error": throw Error(json.writeError) + default: throw Error("unexpected chat_write_file result: " + res) + } +} + +export interface APIResult { + result?: R + error?: T.ChatError +} + +export class ChatAPIError extends Error { + constructor(public message: string, public chatError: T.ChatError | undefined = undefined) { + super(message) + } +} + +/** + * Migration confirmation mode + */ +export enum MigrationConfirmation { + YesUp = "yesUp", + YesUpDown = "yesUpDown", + Console = "console", + Error = "error" +} + +/** + * File encryption key and nonce + */ +export interface CryptoArgs { + fileKey: string + fileNonce: string +} + +export class ChatInitError extends Error { + constructor(public message: string, public dbMigrationError: DBMigrationError) { + super(message) + } +} + +export type DBMigrationError = + | DBMigrationError.InvalidConfirmation + | DBMigrationError.ErrorNotADatabase // invalid/corrupt database file or incorrect encryption key + | DBMigrationError.ErrorMigration + | DBMigrationError.ErrorSQL + +export namespace DBMigrationError { + export type Tag = "invalidConfirmation" | "errorNotADatabase" | "errorMigration" | "errorSQL" + + interface Interface { + type: Tag + } + + export interface InvalidConfirmation extends Interface { + type: "invalidConfirmation" + } + + export interface ErrorNotADatabase extends Interface { + type: "errorNotADatabase" + dbFile: string + } + + export interface ErrorMigration extends Interface { + type: "errorMigration" + dbFile: string + migrationError: MigrationError + } + + export interface ErrorSQL extends Interface { + type: "errorSQL" + dbFile: string + migrationSQLError: string + } +} + +export type MigrationError = + | MigrationError.MEUpgrade + | MigrationError.MEDowngrade + | MigrationError.MigrationError + +export namespace MigrationError { + export type Tag = "upgrade" | "downgrade" | "migrationError" + + interface Interface { + type: Tag + } + + export interface MEUpgrade extends Interface { + type: "upgrade" + upMigrations: UpMigration + } + + export interface MEDowngrade extends Interface { + type: "downgrade" + downMigrations: string[] + } + + export interface MigrationError extends Interface { + type: "migrationError" + mtrError: MTRError + } +} + +export interface UpMigration { + upName: string + withDown: boolean +} + +export type MTRError = + | MTRError.MTRENoDown + | MTRError.MTREDifferent + +export namespace MTRError { + export type Tag = "noDown" | "different" + + interface Interface { + type: Tag + } + + export interface MTRENoDown extends Interface { + type: "noDown" + upMigrations: UpMigration + } + + export interface MTREDifferent extends Interface { + type: "different" + downMigrations: string[] + } +} diff --git a/packages/simplex-chat-nodejs/src/download-libs.js b/packages/simplex-chat-nodejs/src/download-libs.js new file mode 100644 index 0000000000..6b8f583155 --- /dev/null +++ b/packages/simplex-chat-nodejs/src/download-libs.js @@ -0,0 +1,224 @@ +const https = require('https'); +const fs = require('fs'); +const path = require('path'); +const extract = require('extract-zip'); + +const GITHUB_REPO = 'simplex-chat/simplex-chat-libs'; +const RELEASE_TAG = 'v6.5.0-beta.4'; +const ROOT_DIR = process.cwd(); // Root of the package being installed +const LIBS_DIR = path.join(ROOT_DIR, 'libs') +const INSTALLED_FILE = path.join(LIBS_DIR, 'installed.txt'); + +// Detect platform and architecture +function getPlatformInfo() { + const platform = process.platform; + const arch = process.arch; + + let platformName; + let archName; + + if (platform === 'linux') { + platformName = 'linux'; + } else if (platform === 'darwin') { + platformName = 'macos'; + } else if (platform === 'win32') { + platformName = 'windows'; + } else { + throw new Error(`Unsupported platform: ${platform}`); + } + + if (arch === 'x64') { + archName = 'x86_64'; + } else if (arch === 'arm64') { + archName = 'aarch64'; + } else { + throw new Error(`Unsupported architecture: ${arch}`); + } + + return { platformName, archName }; +} + +// Cleanup on libs version mismatch +function cleanLibsDirectory() { + if (fs.existsSync(LIBS_DIR)) { + console.log('Cleaning old libraries...'); + fs.rmSync(LIBS_DIR, { recursive: true, force: true }); + fs.mkdirSync(LIBS_DIR, { recursive: true }); + console.log('✓ Old libraries removed'); + } +} + +// Check if libraries are already installed with the correct version +function isAlreadyInstalled() { + if (!fs.existsSync(INSTALLED_FILE)) { + return false; + } + + try { + const installedVersion = fs.readFileSync(INSTALLED_FILE, 'utf-8').trim(); + if (installedVersion === RELEASE_TAG) { + console.log(`✓ Libraries version ${RELEASE_TAG} already installed`); + return true; + } else { + console.log(`Version mismatch: installed ${installedVersion}, need ${RELEASE_TAG}`); + cleanLibsDirectory(); + return false; + } + } catch (err) { + console.warn(`Could not read installed.txt: ${err.message}`); + return false; + } +} + +async function install() { + try { + // Check if already installed + if (isAlreadyInstalled()) { + return; + } + + const { platformName, archName } = getPlatformInfo(); + const repoName = GITHUB_REPO.split('/')[1]; + const zipFilename = `${repoName}-${platformName}-${archName}.zip`; + const ZIP_URL = `https://github.com/${GITHUB_REPO}/releases/download/${RELEASE_TAG}/${zipFilename}`; + const ZIP_PATH = path.join(ROOT_DIR, zipFilename); + const TEMP_EXTRACT_DIR = path.join(ROOT_DIR, '.temp-extract'); + + console.log(`Detected: ${platformName} ${archName}`); + console.log(`Downloading: ${zipFilename}`); + + // Create libs directory + if (!fs.existsSync(LIBS_DIR)) { + fs.mkdirSync(LIBS_DIR, { recursive: true }); + } + + // Download zip with error handling + await downloadFile(ZIP_URL, ZIP_PATH); + + // Extract to temporary directory + console.log('Extracting to temporary directory...'); + if (!fs.existsSync(TEMP_EXTRACT_DIR)) { + fs.mkdirSync(TEMP_EXTRACT_DIR, { recursive: true }); + } + await extract(ZIP_PATH, { dir: TEMP_EXTRACT_DIR }); + + // Move libs folder contents to final location + console.log('Moving libraries to libs/...'); + const libsSourcePath = path.join(TEMP_EXTRACT_DIR, 'libs'); + + if (fs.existsSync(libsSourcePath)) { + // Copy all files from libs folder to LIBS_DIR + const files = fs.readdirSync(libsSourcePath); + files.forEach(file => { + const src = path.join(libsSourcePath, file); + const dest = path.join(LIBS_DIR, file); + + if (fs.statSync(src).isDirectory()) { + copyDirSync(src, dest); + } else { + fs.copyFileSync(src, dest); + } + }); + } else { + throw new Error('libs folder not found in zip archive'); + } + + // Write installed.txt with version + fs.writeFileSync(INSTALLED_FILE, RELEASE_TAG, 'utf-8'); + console.log(`✓ Wrote version ${RELEASE_TAG} to installed.txt`); + + // Cleanup + fs.rmSync(TEMP_EXTRACT_DIR, { recursive: true, force: true }); + fs.unlinkSync(ZIP_PATH); + console.log('✓ Installation complete'); + } catch (err) { + console.error('✗ Failed:', err.message); + process.exit(1); + } +} + +// Helper function to recursively copy directories +function copyDirSync(src, dest) { + if (!fs.existsSync(dest)) { + fs.mkdirSync(dest, { recursive: true }); + } + const files = fs.readdirSync(src); + files.forEach(file => { + const srcFile = path.join(src, file); + const destFile = path.join(dest, file); + if (fs.statSync(srcFile).isDirectory()) { + copyDirSync(srcFile, destFile); + } else { + fs.copyFileSync(srcFile, destFile); + } + }); +} + +function downloadFile(url, dest) { + return new Promise((resolve, reject) => { + const file = fs.createWriteStream(dest); + + https.get(url, { headers: { 'User-Agent': 'Node.js' } }, (response) => { + // Handle redirects + if (response.statusCode === 302 || response.statusCode === 301) { + file.destroy(); + fs.unlink(dest, () => {}); + return downloadFile(response.headers.location, dest) + .then(resolve) + .catch(reject); + } + + // Handle 404 + if (response.statusCode === 404) { + file.destroy(); + fs.unlink(dest, () => {}); + reject(new Error( + `Release artifact not found (404). Check:\n` + + ` - Repository exists: ${url.split('/releases')[0]}\n` + + ` - Release tag exists: ${RELEASE_TAG}\n` + + ` - Artifact filename is correct` + )); + return; + } + + // Handle 403 + if (response.statusCode === 403) { + file.destroy(); + fs.unlink(dest, () => {}); + reject(new Error( + `Access denied (403). The repository may be private.\n` + + `Set GITHUB_TOKEN environment variable for private repos.` + )); + return; + } + + // Handle other HTTP errors + if (response.statusCode < 200 || response.statusCode >= 300) { + file.destroy(); + fs.unlink(dest, () => {}); + reject(new Error( + `HTTP ${response.statusCode}: Failed to download from ${url}` + )); + return; + } + + response.pipe(file); + + file.on('finish', () => { + file.close(); + resolve(); + }); + + file.on('error', (err) => { + fs.unlink(dest, () => {}); + reject(new Error(`File write error: ${err.message}`)); + }); + }).on('error', (err) => { + file.destroy(); + fs.unlink(dest, () => {}); + reject(new Error(`Download error: ${err.message}`)); + }); + }); +} + +install(); diff --git a/packages/simplex-chat-nodejs/src/index.ts b/packages/simplex-chat-nodejs/src/index.ts new file mode 100644 index 0000000000..80180ae282 --- /dev/null +++ b/packages/simplex-chat-nodejs/src/index.ts @@ -0,0 +1,22 @@ +/** + * A simple declarative API to run a chat-bot with a single function call. + * It automates creating and updating of the bot profile, address and bot commands shown in the app UI. + */ +export * as bot from "./bot" + +/** + * An API to send chat commands and receive chat events to/from chat core. + * You need to use it in bot event handlers, and for any other use cases. + */ +export * as api from "./api" + +/** + * A low level API to the core library - the same that is used in desktop clients. + * You are unlikely to ever need to use this module directly. + */ +export * as core from "./core" + +/** + * Useful functions for chat events and types. + */ +export * as util from "./util" diff --git a/packages/simplex-chat-nodejs/src/simplex.d.ts b/packages/simplex-chat-nodejs/src/simplex.d.ts new file mode 100644 index 0000000000..10c2f6608a --- /dev/null +++ b/packages/simplex-chat-nodejs/src/simplex.d.ts @@ -0,0 +1,10 @@ +// These functions are defined in CPP add-on ../cpp/simplex.cc + +export function chat_migrate_init(dbPath: string, dbKey: string, confirm: string): Promise<[bigint, string]> +export function chat_close_store(ctrl: bigint): Promise +export function chat_send_cmd(ctrl: bigint, cmd: string): Promise +export function chat_recv_msg_wait(ctrl: bigint, wait: number): Promise +export function chat_write_file(ctrl: bigint, path: string, buffer: ArrayBuffer): Promise +export function chat_read_file(path: string, key: string, nonce: string): Promise +export function chat_encrypt_file(ctrl: bigint, fromPath: string, toPath: string): Promise +export function chat_decrypt_file(fromPath: string, key: string, nonce: string, toPath: string): Promise diff --git a/packages/simplex-chat-nodejs/src/simplex.js b/packages/simplex-chat-nodejs/src/simplex.js new file mode 100644 index 0000000000..fd26c67438 --- /dev/null +++ b/packages/simplex-chat-nodejs/src/simplex.js @@ -0,0 +1 @@ +module.exports = require("../build/Release/simplex") diff --git a/packages/simplex-chat-nodejs/src/util.ts b/packages/simplex-chat-nodejs/src/util.ts new file mode 100644 index 0000000000..52e7843a95 --- /dev/null +++ b/packages/simplex-chat-nodejs/src/util.ts @@ -0,0 +1,92 @@ +import {T} from "@simplex-chat/types" +import {BotAddressSettings} from "./api" + +export function chatInfoRef(cInfo: T.ChatInfo): T.ChatRef | undefined { + switch (cInfo.type) { + case T.ChatType.Direct: return {chatType: T.ChatType.Direct, chatId: cInfo.contact.contactId} + case T.ChatType.Group: { + const chatScope: T.GroupChatScope | undefined = + cInfo.groupChatScope?.type == "memberSupport" + ? {type: "memberSupport", groupMemberId_: cInfo.groupChatScope.groupMember_?.groupMemberId} + : undefined + return {chatType: T.ChatType.Group, chatId: cInfo.groupInfo.groupId, chatScope} + } + default: return undefined + } +} + +export function chatInfoName(cInfo: T.ChatInfo): string { + switch (cInfo.type) { + case "direct": return `@${cInfo.contact.profile.displayName}` + case "group": { + const scope = cInfo.groupChatScope + const scopeName = scope?.type === "memberSupport" + ? `(support${scope.groupMember_ ? ` ${scope.groupMember_.memberProfile.displayName}` : ""})` + : "" + return `#${cInfo.groupInfo.groupProfile.displayName}${scopeName}` + } + case "local": return "private notes" + case "contactRequest": return `request from @${cInfo.contactRequest.profile.displayName}` + case "contactConnection": { + const alias = cInfo.contactConnection.localAlias + return `pending connection${alias ? ` (@${alias})` : ""}` + } + } +} + +export function senderName(cInfo: T.ChatInfo, chatDir: T.CIDirection) { + const sender = chatDir.type === "groupRcv" + ? ` @${chatDir.groupMember.memberProfile.displayName}` + : "" + return chatInfoName(cInfo) + sender +} + +export function contactAddressStr(link: T.CreatedConnLink): string { + return link.connShortLink || link.connFullLink +} + +export function botAddressSettings({addressSettings}: T.UserContactLink): BotAddressSettings { + return { + autoAccept: addressSettings.autoAccept ? true : false, + welcomeMessage: addressSettings.autoReply, + businessAddress: addressSettings.businessAddress + } +} + +export function fromLocalProfile({displayName, fullName, shortDescr, image, contactLink, preferences, peerType}: T.LocalProfile): T.Profile { + const profile = {displayName, fullName, shortDescr, image, contactLink, preferences, peerType} + for (const key in profile) { + if (typeof (profile as any)[key] === "undefined") delete (profile as any)[key] + } + return profile +} + +export function ciContentText({content}: T.ChatItem): string | undefined { + switch (content.type) { + case "sndMsgContent": return content.msgContent.text; + case "rcvMsgContent": return content.msgContent.text; + default: return undefined; + } +} + +export interface BotCommand { + keyword: string + params: string +} + +// returns command (without /) and trimmed parameters +export function ciBotCommand(chatItem: T.ChatItem): BotCommand | undefined { + const msg = ciContentText(chatItem)?.trim() + if (msg) { + const r = msg.match(/\/([^\s]+)(.*)/) + if (r && r.length >= 3) { + return {keyword: r[1], params: r[2].trim()} + } + } + return undefined +} + +export function reactionText(reaction: T.ACIReaction): string { + const r = reaction.chatReaction + return r.reaction.type === "emoji" ? r.reaction.emoji : r.reaction.tag +} diff --git a/packages/simplex-chat-nodejs/tests/api.test.ts b/packages/simplex-chat-nodejs/tests/api.test.ts new file mode 100644 index 0000000000..52153ecfed --- /dev/null +++ b/packages/simplex-chat-nodejs/tests/api.test.ts @@ -0,0 +1,67 @@ +import * as path from "path" +import * as fs from "fs" +import {CEvt, T} from "@simplex-chat/types" +import {api} from ".." + +const CT = T.ChatType + +describe("API tests (use preset servers)", () => { + const tmpDir = "./tests/tmp" + const alicePath = path.join(tmpDir, "alice") + const bobPath = path.join(tmpDir, "bob") + + beforeEach(() => fs.mkdirSync(tmpDir, {recursive: true})) + afterEach(() => fs.rmSync(tmpDir, {recursive: true, force: true})) + + it("should send/receive message", async () => { + // create users and start chat controllers + const alice = await api.ChatApi.init(alicePath) + const bob = await api.ChatApi.init(bobPath) + const servers: string[] = [] + let eventCount = 0 + alice.on("hostConnected" as CEvt.Tag, async ({transportHost}: any) => { servers.push(transportHost) }) + alice.onAny(async () => { eventCount++ }) + await expect(alice.apiGetActiveUser()).resolves.toBeUndefined() + const aliceUser = await alice.apiCreateActiveUser({displayName: "alice", fullName: ""}) + await expect(alice.apiGetActiveUser()).resolves.toMatchObject(aliceUser) + await bob.apiCreateActiveUser({displayName: "bob", fullName: ""}) + await alice.startChat() + await bob.startChat() + // connect via link + const link = await alice.apiCreateLink(aliceUser.userId) + await expect(bob.apiConnectActiveUser(link)).resolves.toBe(api.ConnReqType.Invitation) + const [bobContact, aliceContact] = await Promise.all([ + (await alice.wait("contactConnected")).contact, + (await bob.wait("contactConnected")).contact + ]) + expect(bobContact).toMatchObject({profile: {displayName: "bob"}}) + expect(aliceContact).toMatchObject({profile: {displayName: "alice"}}) + // exchange messages + const isMessage = ({contactId}: T.Contact, msg: string) => (evt: CEvt.NewChatItems) => + evt.chatItems.some(ci => ci.chatInfo.type === CT.Direct && ci.chatInfo.contact.contactId === contactId && ci.chatItem.meta.itemText === msg) + await alice.apiSendTextMessage([CT.Direct, bobContact.contactId], "hello") + await bob.wait("newChatItems", isMessage(aliceContact, "hello")) + await bob.apiSendTextMessage([CT.Direct, aliceContact.contactId], "hello too") + await alice.wait("newChatItems", isMessage(bobContact, "hello too"), 10000) + await alice.apiSendTextMessage([CT.Direct, bobContact.contactId], "how are you?") + await bob.wait("newChatItems", isMessage(aliceContact, "how are you?")) + await bob.apiSendTextMessage([CT.Direct, aliceContact.contactId], "ok, and you?") + await alice.wait("newChatItems", isMessage(bobContact, "ok, and you?"), 10000) + // no more messages + await expect(alice.wait("newChatItems", 500)).resolves.toBeUndefined() + await expect(bob.wait("newChatItems", 500)).resolves.toBeUndefined() + // delete contacts, stop chat controllers and close databases + await alice.apiDeleteChat(CT.Direct, bobContact.contactId) + await bob.wait("contactDeletedByContact") + await bob.apiDeleteChat(CT.Direct, aliceContact.contactId) + await alice.stopChat() + await bob.stopChat() + await alice.close() + await bob.close() + await expect(alice.startChat).rejects.toThrow() + await expect(bob.startChat).rejects.toThrow() + expect(servers.length).toBe(2) + expect(servers[0] !== servers[1]).toBe(true) + expect(eventCount > 0).toBe(true) + }, 30000) +}) diff --git a/packages/simplex-chat-nodejs/tests/bot.test.ts b/packages/simplex-chat-nodejs/tests/bot.test.ts new file mode 100644 index 0000000000..b1fd9d0186 --- /dev/null +++ b/packages/simplex-chat-nodejs/tests/bot.test.ts @@ -0,0 +1,60 @@ +import * as path from "path" +import * as fs from "fs" +import * as assert from "assert" +import {CEvt, T} from "@simplex-chat/types" +import {api, bot, util} from ".." + +const CT = T.ChatType + +describe("Bot tests (use preset servers)", () => { + const tmpDir = "./tests/tmp" + const botPath = path.join(tmpDir, "bot") + const alicePath = path.join(tmpDir, "alice") + + beforeEach(() => fs.mkdirSync(tmpDir, {recursive: true})) + afterEach(() => fs.rmSync(tmpDir, {recursive: true, force: true})) + + it("should reply to messages", async () => { + // run bot + const [chat, botUser, botAddress] = await bot.run({ + profile: {displayName: "Squaring bot", fullName: ""}, + dbOpts: {dbFilePrefix: botPath, dbKey: ""}, + options: { + addressSettings: {welcomeMessage: "If you send me a number, I will calculate its square."}, + }, + onMessage: async (ci, content) => { + const n = +content.text + const reply = typeof n === "number" && !isNaN(n) ? `${n} * ${n} = ${n * n}` : `this is not a number` + await chat.apiSendTextReply(ci, reply) + } + }) + assert(typeof botAddress === "object") + // create user + const alice = await api.ChatApi.init(alicePath) + const aliceUser = await alice.apiCreateActiveUser({displayName: "alice", fullName: ""}) + await alice.startChat() + // connect to bot + const [plan, link] = await alice.apiConnectPlan(aliceUser.userId, util.contactAddressStr(botAddress.connLinkContact)) + assert(plan.type === "contactAddress") + await expect(alice.apiConnect(aliceUser.userId, false, link)).resolves.toBe(api.ConnReqType.Contact) + const [botContact, aliceContact] = await Promise.all([ + (await alice.wait("contactConnected")).contact, + (await chat.wait("contactConnected")).contact + ]) + expect(botContact.profile.displayName).toBe("Squaring bot") + // send message to bot + const isMessage = ({contactId}: T.Contact, msg: string) => (evt: CEvt.NewChatItems) => + evt.chatItems.some(ci => ci.chatInfo.type === CT.Direct && ci.chatInfo.contact.contactId === contactId && ci.chatItem.meta.itemText === msg) + await alice.apiSendTextMessage([CT.Direct, botContact.contactId], "2") + console.log("after sending message") + await alice.wait("newChatItems", isMessage(botContact, "2 * 2 = 4"), 5000) + // cleanup + await alice.apiDeleteChat(CT.Direct, botContact.contactId) + await chat.wait("contactDeletedByContact", ({contact}) => contact.contactId === aliceContact.contactId) + await chat.apiDeleteUserAddress(botUser.userId) + await chat.stopChat() + await chat.close() + await alice.stopChat() + await alice.close() + }, 30000) +}) diff --git a/packages/simplex-chat-nodejs/tests/core.test.ts b/packages/simplex-chat-nodejs/tests/core.test.ts new file mode 100644 index 0000000000..141f35746d --- /dev/null +++ b/packages/simplex-chat-nodejs/tests/core.test.ts @@ -0,0 +1,85 @@ +import * as fs from "fs"; +import * as path from "path"; +import {core} from "../src/index"; + +describe("Core tests", () => { + const tmpDir = "./tests/tmp"; + const dbPath = path.join(tmpDir, "simplex_v1"); + + beforeEach(() => fs.mkdirSync(tmpDir, {recursive: true})); + afterEach(() => fs.rmSync(tmpDir, {recursive: true, force: true})); + + it("should initialize chat controller", async () => { + const ctrl = await core.chatMigrateInit(dbPath, "key", core.MigrationConfirmation.YesUp); + expect(typeof ctrl).toBe("bigint"); + await expect(core.chatCloseStore(ctrl)).resolves.toBe(undefined); + + await expect(core.chatMigrateInit(dbPath, "wrong_key", core.MigrationConfirmation.YesUp)).rejects.toMatchObject({ + message: "Database or migration error (see dbMigrationError property)", + dbMigrationError: expect.objectContaining({type: "errorNotADatabase"}) + }); + }); + + it("should send command and receive event", async () => { + const ctrl = await core.chatMigrateInit(dbPath, "key", core.MigrationConfirmation.YesUp); + + await expect(core.chatSendCmd(ctrl, "/v")).resolves.toMatchObject({ + type: "versionInfo" + }); + await expect(core.chatSendCmd(ctrl, '/debug event {"type": "chatSuspended"}')).resolves.toMatchObject({ + type: "cmdOk" + }); + + const wait = 500_000; + await expect(core.chatRecvMsgWait(ctrl, wait)).resolves.toMatchObject({ + type: "chatSuspended" + }); + await expect(core.chatRecvMsgWait(ctrl, wait)).resolves.toBe(undefined); + + await expect(core.chatSendCmd(ctrl, "/unknown")).rejects.toMatchObject({ + message: "Chat command error (see chatError property)", + chatError: expect.objectContaining({type: "error"}) + }); + + await core.chatCloseStore(ctrl); + }); + + it("should write/read encrypted file from/to buffer", async () => { + const ctrl = await core.chatMigrateInit(dbPath, "key", core.MigrationConfirmation.YesUp); + + const filePath = path.join(tmpDir, "write_file.txt"); + const buffer = new Uint8Array([0, 1, 2]).buffer; + const cryptoArgs = await core.chatWriteFile(ctrl, filePath, buffer); + expect(typeof cryptoArgs.fileKey).toBe("string"); + expect(typeof cryptoArgs.fileNonce).toBe("string"); + + const buffer2 = await core.chatReadFile(filePath, cryptoArgs); + expect(Buffer.from(buffer2).equals(Buffer.from(buffer))).toBe(true); + + await expect(core.chatWriteFile(ctrl, path.join(tmpDir, "unknown", "unknown.txt"), buffer)).rejects.toThrow(); + await expect(core.chatReadFile(path.join(tmpDir, "unknown.txt"), cryptoArgs)).rejects.toThrow(); + + await core.chatCloseStore(ctrl); + }); + + it("should encrypt/decrypt file", async () => { + const ctrl = await core.chatMigrateInit(dbPath, "key", core.MigrationConfirmation.YesUp); + + const unencryptedPath = path.join(tmpDir, "file_unencrypted.txt"); + fs.writeFileSync(unencryptedPath, "unencrypted\n"); + const encryptedPath = path.join(tmpDir, "file_encrypted.txt"); + const cryptoArgs = await core.chatEncryptFile(ctrl, unencryptedPath, encryptedPath); + expect(typeof cryptoArgs.fileKey).toBe("string"); + expect(typeof cryptoArgs.fileNonce).toBe("string"); + + const decryptedPath: string = path.join(tmpDir, "file_decrypted.txt"); + await expect(core.chatDecryptFile(encryptedPath, cryptoArgs, decryptedPath)).resolves.toBe(undefined); + + expect(fs.readFileSync(decryptedPath, "utf8")).toBe("unencrypted\n"); + + await expect(core.chatEncryptFile(ctrl, path.join(tmpDir, "unknown.txt"), encryptedPath)).rejects.toThrow(); + await expect(core.chatDecryptFile(path.join(tmpDir, "unknown.txt"), cryptoArgs, decryptedPath)).rejects.toThrow(); + + await core.chatCloseStore(ctrl); + }); +}); diff --git a/packages/simplex-chat-nodejs/tests/tsconfig.json b/packages/simplex-chat-nodejs/tests/tsconfig.json new file mode 100644 index 0000000000..47e973f3af --- /dev/null +++ b/packages/simplex-chat-nodejs/tests/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "types": ["node", "jest"] + }, + "include": ["**/*", "../src/**/*"] +} diff --git a/packages/simplex-chat-nodejs/tsconfig.json b/packages/simplex-chat-nodejs/tsconfig.json new file mode 100644 index 0000000000..f5dc986431 --- /dev/null +++ b/packages/simplex-chat-nodejs/tsconfig.json @@ -0,0 +1,23 @@ +{ + "include": ["src"], + "compilerOptions": { + "declaration": true, + "forceConsistentCasingInFileNames": true, + "lib": ["ES2018"], + "module": "CommonJS", + "moduleResolution": "Node", + "noFallthroughCasesInSwitch": true, + "noImplicitAny": true, + "noImplicitReturns": true, + "noImplicitThis": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noEmitOnError": true, + "outDir": "dist", + "sourceMap": true, + "strict": true, + "strictNullChecks": true, + "target": "ES2018", + "types": ["node"] + } +} diff --git a/packages/simplex-chat-nodejs/typedoc.json b/packages/simplex-chat-nodejs/typedoc.json new file mode 100644 index 0000000000..3523115f49 --- /dev/null +++ b/packages/simplex-chat-nodejs/typedoc.json @@ -0,0 +1,14 @@ +{ + "name": "simplex-chat", + "plugin": ["typedoc-plugin-markdown"], + "entryPoints": [ + "./src/index.ts", + "../simplex-chat-client/types/typescript/src/index.ts" + ], + "entryPointStrategy": "expand", + "tsconfig": "./tsconfig.json", + "sourceLinkTemplate": "../{path}#L{line}", + "disableGit": true, + "flattenOutputFiles": true, + "out": "./docs" +} \ No newline at end of file From 8800f5e62fa1c9f8a0744215db7fd3e7b7d47554 Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Thu, 15 Jan 2026 09:36:53 +0000 Subject: [PATCH 21/73] core: correctly handle errors in withLocalDisplayName for postgres (rollback to savepoint) (#6577) --- src/Simplex/Chat/Store/Shared.hs | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/src/Simplex/Chat/Store/Shared.hs b/src/Simplex/Chat/Store/Shared.hs index 15ec3ec49d..c9dc87b5fa 100644 --- a/src/Simplex/Chat/Store/Shared.hs +++ b/src/Simplex/Chat/Store/Shared.hs @@ -581,7 +581,7 @@ withLocalDisplayName db userId displayName action = getLdnSuffix >>= (`tryCreate tryCreateName ldnSuffix attempts = do currentTs <- getCurrentTime let ldn = displayName <> (if ldnSuffix == 0 then "" else T.pack $ '_' : show ldnSuffix) - E.try (insertName ldn currentTs) >>= \case + withSavepoint db "ldn_insert" (insertName ldn currentTs) >>= \case Right () -> action ldn Left e | constraintError e -> tryCreateName (ldnSuffix + 1) (attempts - 1) @@ -597,6 +597,25 @@ withLocalDisplayName db userId displayName action = getLdnSuffix >>= (`tryCreate |] (ldn, displayName, ldnSuffix, userId, ts, ts) +-- Execute an action within a savepoint (PostgreSQL only). +-- On success, releases the savepoint. On error, rolls back to the savepoint +-- to restore the transaction to a usable state before returning the error. +withSavepoint :: DB.Connection -> Query -> IO a -> IO (Either SQLError a) +withSavepoint db name action = +#if defined(dbPostgres) + do + DB.execute_ db $ "SAVEPOINT " <> name + E.try action >>= \case + Right r -> do + DB.execute_ db $ "RELEASE SAVEPOINT " <> name + pure $ Right r + Left e -> do + DB.execute_ db $ "ROLLBACK TO SAVEPOINT " <> name + pure $ Left e +#else + E.try action +#endif + createWithRandomId :: forall a. TVar ChaChaDRG -> (ByteString -> IO a) -> ExceptT StoreError IO a createWithRandomId = createWithRandomBytes 12 From a8d7a9b3894f247019d8c8bd4a3c91dc60ab2f9b Mon Sep 17 00:00:00 2001 From: Alexandre Esteves <2335822+alexfmpe@users.noreply.github.com> Date: Thu, 15 Jan 2026 09:51:54 +0000 Subject: [PATCH 22/73] scripts: more portable (#6562) * scripts/desktop: use more portable shebang on linux * scripts/desktop: only query uname for architecture * scripts/desktop: don't hardcode ghc version * revert GHC version * Apply suggestions from code review * accept arch as param --------- Co-authored-by: Evgeny --- scripts/desktop/build-lib-linux.sh | 2 +- scripts/desktop/build-lib-mac.sh | 2 +- scripts/desktop/make-appimage-linux.sh | 2 +- scripts/desktop/prepare-vlc-linux.sh | 2 +- scripts/desktop/prepare-vlc-mac.sh | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/scripts/desktop/build-lib-linux.sh b/scripts/desktop/build-lib-linux.sh index a9deb28d9a..7868a125b6 100755 --- a/scripts/desktop/build-lib-linux.sh +++ b/scripts/desktop/build-lib-linux.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash set -e diff --git a/scripts/desktop/build-lib-mac.sh b/scripts/desktop/build-lib-mac.sh index 782610b302..6013fc55c0 100755 --- a/scripts/desktop/build-lib-mac.sh +++ b/scripts/desktop/build-lib-mac.sh @@ -3,7 +3,7 @@ set -e OS=mac -ARCH="${1:-`uname -a | rev | cut -d' ' -f1 | rev`}" +ARCH="${1:-$(uname -m)}" COMPOSE_ARCH=$ARCH GHC_VERSION=9.6.3 DATABASE_BACKEND="${2:-sqlite}" diff --git a/scripts/desktop/make-appimage-linux.sh b/scripts/desktop/make-appimage-linux.sh index 5978fe0cba..c242b63d54 100755 --- a/scripts/desktop/make-appimage-linux.sh +++ b/scripts/desktop/make-appimage-linux.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash set -e diff --git a/scripts/desktop/prepare-vlc-linux.sh b/scripts/desktop/prepare-vlc-linux.sh index 6106035d83..ef1ee1b308 100755 --- a/scripts/desktop/prepare-vlc-linux.sh +++ b/scripts/desktop/prepare-vlc-linux.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash set -e diff --git a/scripts/desktop/prepare-vlc-mac.sh b/scripts/desktop/prepare-vlc-mac.sh index 4db2983f67..180acf4426 100755 --- a/scripts/desktop/prepare-vlc-mac.sh +++ b/scripts/desktop/prepare-vlc-mac.sh @@ -2,7 +2,7 @@ set -e -ARCH="${1:-`uname -a | rev | cut -d' ' -f1 | rev`}" +ARCH="${1:-$(uname -m)}" if [ "$ARCH" == "arm64" ]; then ARCH=aarch64 vlc_arch=arm64 From 2fc72861e27876d3e71f13708c555db447ef164a Mon Sep 17 00:00:00 2001 From: Evgeny Date: Thu, 15 Jan 2026 14:47:50 +0000 Subject: [PATCH 23/73] multiplatform/common: catch every exception at base64ToBitmap (#6576) Co-authored-by: shum --- .../kotlin/chat/simplex/common/platform/Images.desktop.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Images.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Images.desktop.kt index d0ba082adf..d3b8cdcb58 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Images.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Images.desktop.kt @@ -27,7 +27,7 @@ actual fun base64ToBitmap(base64ImageString: String): ImageBitmap { .removePrefix("data:image/jpg;base64,") return try { ImageIO.read(ByteArrayInputStream(Base64.getMimeDecoder().decode(imageString))).toComposeImageBitmap() - } catch (e: IOException) { + } catch (e: Throwable) { Log.e(TAG, "base64ToBitmap error: $e") errorBitmap() } From 2d64365d8c4190027f544322b727d47d4458b375 Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Thu, 15 Jan 2026 15:18:15 +0000 Subject: [PATCH 24/73] core: correctly handle errors in createWithRandomId/Bytes for postgres (reuse withSavepoint from simplexmq) (#6578) --- cabal.project | 2 +- scripts/nix/sha256map.nix | 2 +- src/Simplex/Chat/Store/Groups.hs | 10 ++++---- src/Simplex/Chat/Store/Messages.hs | 9 ++++--- src/Simplex/Chat/Store/Shared.hs | 38 ++++++++---------------------- 5 files changed, 23 insertions(+), 38 deletions(-) diff --git a/cabal.project b/cabal.project index 2f478dbc0f..390890d258 100644 --- a/cabal.project +++ b/cabal.project @@ -12,7 +12,7 @@ constraints: zip +disable-bzip2 +disable-zstd source-repository-package type: git location: https://github.com/simplex-chat/simplexmq.git - tag: 58212c421aa6abb6ad894b8231d8a380849b704b + tag: ca26c69937083deee43b8b2200ec9ef4c004ceac source-repository-package type: git diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index f080cb1118..31166762bc 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,5 +1,5 @@ { - "https://github.com/simplex-chat/simplexmq.git"."58212c421aa6abb6ad894b8231d8a380849b704b" = "1awgvhqfi7gv3xl10h21a6w2hhqc48pq6yq4f83awg1zxkh3hiqn"; + "https://github.com/simplex-chat/simplexmq.git"."ca26c69937083deee43b8b2200ec9ef4c004ceac" = "1p7jhxcbn95kddfwa5rjpzfx78fzic03wmy9dmh1mj3j14vyfn02"; "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/Store/Groups.hs b/src/Simplex/Chat/Store/Groups.hs index 5721efb65e..a54a3a6913 100644 --- a/src/Simplex/Chat/Store/Groups.hs +++ b/src/Simplex/Chat/Store/Groups.hs @@ -1133,7 +1133,7 @@ getGroupInvitation db vr user groupId = createNewContactMember :: DB.Connection -> TVar ChaChaDRG -> User -> GroupInfo -> Contact -> GroupMemberRole -> ConnId -> ConnReqInvitation -> SubscriptionMode -> ExceptT StoreError IO GroupMember createNewContactMember _ _ _ _ Contact {localDisplayName, activeConn = Nothing} _ _ _ _ = throwError $ SEContactNotReady localDisplayName createNewContactMember db gVar User {userId, userContactId} GroupInfo {groupId, membership} Contact {contactId, localDisplayName, profile, activeConn = Just Connection {connChatVersion, peerChatVRange}} memberRole agentConnId connRequest subMode = - createWithRandomId' gVar $ \memId -> runExceptT $ do + createWithRandomId' db gVar $ \memId -> runExceptT $ do createdAt <- liftIO getCurrentTime member@GroupMember {groupMemberId} <- createMember_ (MemberId memId) createdAt void $ liftIO $ createMemberConnection_ db userId groupMemberId agentConnId connChatVersion peerChatVRange Nothing 0 createdAt subMode @@ -1188,7 +1188,7 @@ createNewContactMember db gVar User {userId, userContactId} GroupInfo {groupId, createNewContactMemberAsync :: DB.Connection -> TVar ChaChaDRG -> User -> GroupInfo -> Contact -> GroupMemberRole -> (CommandId, ConnId) -> VersionChat -> VersionRangeChat -> SubscriptionMode -> ExceptT StoreError IO () createNewContactMemberAsync db gVar user@User {userId, userContactId} GroupInfo {groupId, membership} Contact {contactId, localDisplayName, profile} memberRole (cmdId, agentConnId) chatV peerChatVRange subMode = - createWithRandomId' gVar $ \memId -> runExceptT $ do + createWithRandomId' db gVar $ \memId -> runExceptT $ do createdAt <- liftIO getCurrentTime insertMember_ (MemberId memId) createdAt groupMemberId <- liftIO $ insertedRowId db @@ -1233,7 +1233,7 @@ createJoiningMember "INSERT INTO contact_profiles (display_name, full_name, short_descr, image, contact_link, user_id, preferences, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?)" (displayName, fullName, shortDescr, image, contactLink, userId, preferences, currentTs, currentTs) profileId <- liftIO $ insertedRowId db - createWithRandomId' gVar $ \memId -> runExceptT $ do + createWithRandomId' db gVar $ \memId -> runExceptT $ do insertMember_ ldn profileId (MemberId memId) currentTs groupMemberId <- liftIO $ insertedRowId db pure (groupMemberId, MemberId memId) @@ -1318,7 +1318,7 @@ createBusinessRequestGroup VersionRange minV maxV = cReqChatVRange insertClientMember_ currentTs groupId membership = ExceptT . withLocalDisplayName db userId displayName $ \localDisplayName -> runExceptT $ do - createWithRandomId' gVar $ \memId -> runExceptT $ do + createWithRandomId' db gVar $ \memId -> runExceptT $ do indexInGroup <- getUpdateNextIndexInGroup_ db groupId liftIO $ DB.execute @@ -1950,7 +1950,7 @@ getMatchingMemberContacts db vr user@User {userId} GroupMember {memberProfile = createSentProbe :: DB.Connection -> TVar ChaChaDRG -> UserId -> ContactOrMember -> ExceptT StoreError IO (Probe, Int64) createSentProbe db gVar userId to = - createWithRandomBytes 32 gVar $ \probe -> do + createWithRandomBytes db 32 gVar $ \probe -> do currentTs <- getCurrentTime let (ctId, gmId) = contactOrMemberIds to DB.execute diff --git a/src/Simplex/Chat/Store/Messages.hs b/src/Simplex/Chat/Store/Messages.hs index 71e8a35386..ce03edbdbd 100644 --- a/src/Simplex/Chat/Store/Messages.hs +++ b/src/Simplex/Chat/Store/Messages.hs @@ -174,6 +174,7 @@ import Simplex.Chat.Types import Simplex.Chat.Types.Shared import Simplex.Messaging.Agent.Protocol (AgentMsgId, ConnId, MsgMeta (..), UserId) import Simplex.Messaging.Agent.Store.AgentStore (firstRow, firstRow', maybeFirstRow) +import Simplex.Messaging.Agent.Store.Common (withSavepoint) import Simplex.Messaging.Agent.Store.DB (BoolInt (..)) import qualified Simplex.Messaging.Agent.Store.DB as DB import qualified Simplex.Messaging.Crypto as C @@ -219,7 +220,7 @@ deleteGroupChatItemsMessages db User {userId} GroupInfo {groupId} = do createNewSndMessage :: MsgEncodingI e => DB.Connection -> TVar ChaChaDRG -> ConnOrGroupId -> ChatMsgEvent e -> (SharedMsgId -> EncodedChatMessage) -> ExceptT StoreError IO SndMessage createNewSndMessage db gVar connOrGroupId chatMsgEvent encodeMessage = - createWithRandomId' gVar $ \sharedMsgId -> + createWithRandomId' db gVar $ \sharedMsgId -> case encodeMessage (SharedMsgId sharedMsgId) of ECMLarge -> pure $ Left SELargeMsg ECMEncoded msgBody -> do @@ -2700,12 +2701,14 @@ updateGroupCIMentions :: DB.Connection -> GroupInfo -> ChatItem 'CTGroup d -> Ma updateGroupCIMentions db g ci@ChatItem {mentions} mentions' | mentions' == mentions = pure ci | otherwise = do - unless (null mentions) $ deleteMentions + unless (null mentions) deleteMentions if null mentions' then pure ci else -- This is a fallback for the error that should not happen in practice. -- In theory, it may happen in item mentions in database are different from item record. - createMentions `E.catch` \e -> if constraintError e then deleteMentions >> createMentions else E.throwIO e + withSavepoint db "create_mentions" createMentions >>= \case + Right r -> pure r + Left e -> if constraintError e then deleteMentions >> createMentions else E.throwIO e where deleteMentions = DB.execute db "DELETE FROM chat_item_mentions WHERE chat_item_id = ?" (Only $ chatItemId' ci) createMentions = createGroupCIMentions db g ci mentions' diff --git a/src/Simplex/Chat/Store/Shared.hs b/src/Simplex/Chat/Store/Shared.hs index c9dc87b5fa..3a89bae47d 100644 --- a/src/Simplex/Chat/Store/Shared.hs +++ b/src/Simplex/Chat/Store/Shared.hs @@ -40,6 +40,7 @@ import Simplex.Chat.Types.UITheme import Simplex.Messaging.Agent.Protocol (AConnShortLink (..), AConnectionRequestUri (..), ACreatedConnLink (..), ConnId, ConnShortLink, ConnectionRequestUri, CreatedConnLink (..), UserId, connMode) import Simplex.Messaging.Agent.Store (AnyStoreError (..)) import Simplex.Messaging.Agent.Store.AgentStore (firstRow, maybeFirstRow) +import Simplex.Messaging.Agent.Store.Common (withSavepoint) import Simplex.Messaging.Agent.Store.DB (BoolInt (..)) import qualified Simplex.Messaging.Agent.Store.DB as DB import qualified Simplex.Messaging.Crypto as C @@ -597,42 +598,23 @@ withLocalDisplayName db userId displayName action = getLdnSuffix >>= (`tryCreate |] (ldn, displayName, ldnSuffix, userId, ts, ts) --- Execute an action within a savepoint (PostgreSQL only). --- On success, releases the savepoint. On error, rolls back to the savepoint --- to restore the transaction to a usable state before returning the error. -withSavepoint :: DB.Connection -> Query -> IO a -> IO (Either SQLError a) -withSavepoint db name action = -#if defined(dbPostgres) - do - DB.execute_ db $ "SAVEPOINT " <> name - E.try action >>= \case - Right r -> do - DB.execute_ db $ "RELEASE SAVEPOINT " <> name - pure $ Right r - Left e -> do - DB.execute_ db $ "ROLLBACK TO SAVEPOINT " <> name - pure $ Left e -#else - E.try action -#endif +createWithRandomId :: forall a. DB.Connection -> TVar ChaChaDRG -> (ByteString -> IO a) -> ExceptT StoreError IO a +createWithRandomId db = createWithRandomBytes db 12 -createWithRandomId :: forall a. TVar ChaChaDRG -> (ByteString -> IO a) -> ExceptT StoreError IO a -createWithRandomId = createWithRandomBytes 12 +createWithRandomId' :: forall a. DB.Connection -> TVar ChaChaDRG -> (ByteString -> IO (Either StoreError a)) -> ExceptT StoreError IO a +createWithRandomId' db = createWithRandomBytes' db 12 -createWithRandomId' :: forall a. TVar ChaChaDRG -> (ByteString -> IO (Either StoreError a)) -> ExceptT StoreError IO a -createWithRandomId' = createWithRandomBytes' 12 +createWithRandomBytes :: forall a. DB.Connection -> Int -> TVar ChaChaDRG -> (ByteString -> IO a) -> ExceptT StoreError IO a +createWithRandomBytes db size gVar create = createWithRandomBytes' db size gVar (fmap Right . create) -createWithRandomBytes :: forall a. Int -> TVar ChaChaDRG -> (ByteString -> IO a) -> ExceptT StoreError IO a -createWithRandomBytes size gVar create = createWithRandomBytes' size gVar (fmap Right . create) - -createWithRandomBytes' :: forall a. Int -> TVar ChaChaDRG -> (ByteString -> IO (Either StoreError a)) -> ExceptT StoreError IO a -createWithRandomBytes' size gVar create = tryCreate 3 +createWithRandomBytes' :: forall a. DB.Connection -> Int -> TVar ChaChaDRG -> (ByteString -> IO (Either StoreError a)) -> ExceptT StoreError IO a +createWithRandomBytes' db size gVar create = tryCreate 3 where tryCreate :: Int -> ExceptT StoreError IO a tryCreate 0 = throwError SEUniqueID tryCreate n = do id' <- liftIO $ encodedRandomBytes gVar size - liftIO (E.try $ create id') >>= \case + liftIO (withSavepoint db "create_random_id" (create id')) >>= \case Right x -> liftEither x Left e | constraintError e -> tryCreate (n - 1) From d3a72473e9217d0a97702d909d9adffe92374d99 Mon Sep 17 00:00:00 2001 From: sh <37271604+shumvgolove@users.noreply.github.com> Date: Thu, 15 Jan 2026 16:36:35 +0000 Subject: [PATCH 25/73] nodejs: add and fix windows (#6581) * simplex-chat-nodejs: adjust binding.gyp for windows * simplex-chat-nodejs: different library linkage for windows * simplex-chat-nodejs: remove non-moving GC in Windows "non-moving GC is broken on windows with GHC 9.4-9.6.3" from: https://github.com/simplex-chat/simplex-chat/blob/master/apps/multiplatform/common/src/commonMain/cpp/desktop/simplex-api.c#L11-L17 * ci: add windows to release-nodejs-libs * simplex-chat-nodejs: same curl flags for dll.def download --- .github/workflows/build.yml | 18 ++++++++++++++++ packages/simplex-chat-nodejs/binding.gyp | 23 +++++++++++++++++---- packages/simplex-chat-nodejs/cpp/simplex.cc | 12 +++++++++++ 3 files changed, 49 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 05bf62f7fa..c882b351e7 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -605,6 +605,9 @@ jobs: - name: Checkout current repository uses: actions/checkout@v6 + - name: Install packages for archiving + run: sudo apt install -y msitools gcc-mingw-w64 + - name: Build archives run: | INIT_DIR='${{ runner.temp }}/artifacts' @@ -612,6 +615,8 @@ jobs: TAG='${{ github.ref_name }}' URL='https://github.com/${{ github.repository }}/releases/download' PREFIX='${{ github.event.repository.name }}-libs' + # Windows-specific + FILE_URL='https://raw.githubusercontent.com/${{ github.repository }}/refs/tags/${{ github.ref_name }}' # Setup directories mkdir "$INIT_DIR" "$RELEASE_DIR" && cd "$INIT_DIR" @@ -620,6 +625,7 @@ jobs: curl --proto '=https' --tlsv1.2 -sSf -L "${URL}/${TAG}/simplex-desktop-ubuntu-22_04-x86_64.deb" -o linux.deb curl --proto '=https' --tlsv1.2 -sSf -L "${URL}/${TAG}/simplex-desktop-macos-aarch64.dmg" -o macos-aarch64.dmg curl --proto '=https' --tlsv1.2 -sSf -L "${URL}/${TAG}/simplex-desktop-macos-x86_64.dmg" -o macos-x86_64.dmg + curl --proto '=https' --tlsv1.2 -sSf -L "${URL}/${TAG}/simplex-desktop-windows-x86_64.msi" -o windows-x86_64.msi # Linux # ----- @@ -646,6 +652,18 @@ jobs: zip -r "${PREFIX}-macos-x86_64.zip" libs mv "${PREFIX}-macos-x86_64.zip" "$RELEASE_DIR" && cd "$INIT_DIR" + # Windows: x86_64 + # --------------- + msiextract windows-x86_64.msi -C windows-out && cd windows-out/SimpleX/app/resources + + # We need to generate library that exports symbols from Windows dll + curl --proto '=https' --tlsv1.2 -sSf -LO "${FILE_URL}/libsimplex.dll.def" + x86_64-w64-mingw32-dlltool -d libsimplex.dll.def -l libsimplex.lib -D libsimplex.dll + + mkdir libs && cp *.dll *.lib libs/ + zip -r "${PREFIX}-windows-x86_64.zip" libs + mv "${PREFIX}-windows-x86_64.zip" "$RELEASE_DIR" && cd "$INIT_DIR" + - name: Create release in libs repo and upload artifacts uses: softprops/action-gh-release@v2 with: diff --git a/packages/simplex-chat-nodejs/binding.gyp b/packages/simplex-chat-nodejs/binding.gyp index addb293f5b..09c63cecba 100644 --- a/packages/simplex-chat-nodejs/binding.gyp +++ b/packages/simplex-chat-nodejs/binding.gyp @@ -9,15 +9,15 @@ "dependencies": [ "(argv); hs_init_with_rtsopts(&argc, &pargv); } From c11ff747b04d1ec32aca9b5b45b7a8590b4449b6 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Thu, 15 Jan 2026 17:17:38 +0000 Subject: [PATCH 26/73] nodejs: update package 6.5.0-beta.4.4 --- packages/simplex-chat-nodejs/README.md | 8 +++++--- .../simplex-chat-nodejs/examples/squaring-bot-readme.js | 2 +- packages/simplex-chat-nodejs/package.json | 2 +- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/simplex-chat-nodejs/README.md b/packages/simplex-chat-nodejs/README.md index 982b8d0553..e75dab3f96 100644 --- a/packages/simplex-chat-nodejs/README.md +++ b/packages/simplex-chat-nodejs/README.md @@ -14,7 +14,7 @@ Please share your use cases and implementations. ## Quick start: a simple bot ``` -npm i simplex-chat@6.5.0-beta.4.2 +npm i simplex-chat@6.5.0-beta.4.4 ``` Simple bot that replies with squares of numbers you send to it: @@ -28,7 +28,7 @@ Simple bot that replies with squares of numbers you send to it: profile: {displayName: "Squaring bot example", fullName: ""}, dbOpts: {dbFilePrefix: "./squaring_bot", dbKey: ""}, options: { - addressSettings: {welcomeMessage: "If you send me a number, I will calculate its square."}, + addressSettings: {welcomeMessage: "Send a number, I will square it.", }, onMessage: async (ci, content) => { const n = +content.text @@ -44,9 +44,11 @@ Simple bot that replies with squares of numbers you send to it: If you installed this package as dependency, you can run this example with: ```sh -node ./node_modules/simplex-chat/examples/squaring-bot-readme.js` +node ./node_modules/simplex-chat/examples/squaring-bot-readme.js ``` +If you run it on Mac, the first time it will take 20-30 seconds for MacOS to verify the library. + If you cloned this repository, you can: ``` diff --git a/packages/simplex-chat-nodejs/examples/squaring-bot-readme.js b/packages/simplex-chat-nodejs/examples/squaring-bot-readme.js index f880808bcf..16d0678b64 100644 --- a/packages/simplex-chat-nodejs/examples/squaring-bot-readme.js +++ b/packages/simplex-chat-nodejs/examples/squaring-bot-readme.js @@ -4,7 +4,7 @@ profile: {displayName: "Squaring bot example", fullName: ""}, dbOpts: {dbFilePrefix: "./squaring_bot", dbKey: ""}, options: { - addressSettings: {welcomeMessage: "If you send me a number, I will calculate its square."}, + addressSettings: {welcomeMessage: "Send a number, I will square it."}, }, onMessage: async (ci, content) => { const n = +content.text diff --git a/packages/simplex-chat-nodejs/package.json b/packages/simplex-chat-nodejs/package.json index 1ef1429e65..498d502edd 100644 --- a/packages/simplex-chat-nodejs/package.json +++ b/packages/simplex-chat-nodejs/package.json @@ -1,6 +1,6 @@ { "name": "simplex-chat", - "version": "6.5.0-beta.4.2", + "version": "6.5.0-beta.4.4", "main": "dist/index.js", "types": "dist/index.d.ts", "files": [ From da02bae85ec7d506f2d850b51a648796fa6a63d4 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Sat, 17 Jan 2026 07:16:23 +0000 Subject: [PATCH 27/73] readme: update SimpleX addresses --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 3364c28284..fc31c522ca 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ ## Connect to the team -You can connect to the team via the app using "chat with the developers button" available when you have no conversations in the profile, "Send questions and ideas" in the app settings or via our [SimpleX address](https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23%2F%3Fv%3D1%26dh%3DMCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion). Please connect to: +You can connect to the team via the app using "chat with the developers button" available when you have no conversations in the profile, "Send questions and ideas" in the app settings or via our [SimpleX address](https://smp6.simplex.im/a#lrdvu2d8A1GumSmoKb2krQmtKhWXq-tyGpHuM7aMwsw). Please connect to: - to ask any questions - to suggest any improvements @@ -54,7 +54,7 @@ If you are interested in helping us to integrate open-source language models, an ## Join user groups -You can find the groups created by users in [SimpleX Directory](https://simplex.chat/directory/). It is also available as [SimpleX bot](https://simplex.chat/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) that allows to add your own groups and communities to the directory. We are not responsible for the content shared in these groups. +You can find the groups created by users in [SimpleX Directory](https://simplex.chat/directory/). It is also available as [SimpleX bot](https://smp4.simplex.im/a#lXUjJW5vHYQzoLYgmi8GbxkGP41_kjefFvBrdwg-0Ok) that allows to add your own groups and communities to the directory. We are not responsible for the content shared in these groups. **Please note**: The groups below are created for the users to be able to ask questions, make suggestions and ask questions about SimpleX Chat only. From c9193fb702bfc83cd642bb9ceac3a86f5d3859d9 Mon Sep 17 00:00:00 2001 From: "M. Sarmad Qadeer" Date: Sat, 17 Jan 2026 14:50:50 +0500 Subject: [PATCH 28/73] website: fixes (#6454) * add rtl support * enable arabic language * fix colors for navbar on RTL language and when scrolling to footer --------- Co-authored-by: Evgeny Poberezkin --- website/src/_data/languages.json | 1 + website/src/_includes/navbar.html | 4 +- website/src/css/design3-nav.css | 97 ++++++++++++++++++++++++++++--- website/src/css/design3.css | 56 ++++++++++++++++-- website/src/index.html | 18 +++--- 5 files changed, 150 insertions(+), 26 deletions(-) diff --git a/website/src/_data/languages.json b/website/src/_data/languages.json index 0736597953..362e8c4120 100644 --- a/website/src/_data/languages.json +++ b/website/src/_data/languages.json @@ -14,6 +14,7 @@ "textColor": "white", "iconBg": "green", "enabled": true, + "home": true, "rtl": true }, { diff --git a/website/src/_includes/navbar.html b/website/src/_includes/navbar.html index 2a75a2769b..003c069d9c 100644 --- a/website/src/_includes/navbar.html +++ b/website/src/_includes/navbar.html @@ -7,8 +7,8 @@ {% endfor %}