diff --git a/cabal.project b/cabal.project index a68df42bf7..9fd0c4928a 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: bd67844169d2206d8543c01e6ed966315115b0e3 + tag: 199f85ec62bcbadd11dc05bb97cce6556dafb384 source-repository-package type: git diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index 468ca71307..2733c6c1eb 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,5 +1,5 @@ { - "https://github.com/simplex-chat/simplexmq.git"."bd67844169d2206d8543c01e6ed966315115b0e3" = "1g218q15hrg21h8gyidavfys5zx8dzmxq7iwfm5bfaw71grpd7pn"; + "https://github.com/simplex-chat/simplexmq.git"."199f85ec62bcbadd11dc05bb97cce6556dafb384" = "0x5c29x5h1832hks0wpxy84x16rn13pi9zd2j9x8lvb3y8h0ddk0"; "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 11c0cc0731..85873c2d1c 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -145,6 +145,7 @@ library Simplex.Chat.Migrations.M20240501_chat_deleted Simplex.Chat.Migrations.M20240510_chat_items_via_proxy Simplex.Chat.Migrations.M20240515_rcv_files_user_approved_relays + Simplex.Chat.Migrations.M20240528_quota_err_counter Simplex.Chat.Mobile Simplex.Chat.Mobile.File Simplex.Chat.Mobile.Shared diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index 77fac9ddc2..c674b6a564 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -1472,14 +1472,14 @@ processChatCommand' vr = \case ct@Contact {activeConn} <- withStore $ \db -> getContact db vr user contactId case activeConn of Just conn -> do - withStore' $ \db -> setConnectionAuthErrCounter db user conn 0 + withStore' $ \db -> setAuthErrCounter db user conn 0 ok user Nothing -> throwChatError $ CEContactNotActive ct APIEnableGroupMember gId gMemberId -> withUser $ \user -> do GroupMember {activeConn} <- withStore $ \db -> getGroupMember db vr user gId gMemberId case activeConn of Just conn -> do - withStore' $ \db -> setConnectionAuthErrCounter db user conn 0 + withStore' $ \db -> setAuthErrCounter db user conn 0 ok user _ -> throwChatError CEGroupMemberNotActive SetShowMessages cName ntfOn -> updateChatSettings cName (\cs -> cs {enableNtfs = ntfOn}) @@ -3960,14 +3960,19 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = withAckMessage' "new contact msg" agentConnId meta $ void $ saveDirectRcvMSG conn meta msgBody - SENT msgId _proxy -> + SENT msgId _proxy -> do + void $ continueSending connEntity conn sentMsgDeliveryEvent conn msgId OK -> -- [async agent commands] continuation on receiving OK when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure () + QCONT -> + void $ continueSending connEntity conn + MWARN _ err -> + processConnMWARN connEntity conn err MERR _ err -> do toView $ CRChatError (Just user) (ChatErrorAgent err $ Just connEntity) - incAuthErrCounter connEntity conn err + processConnMERR connEntity conn err MERRS _ err -> do -- error cannot be AUTH error here toView $ CRChatError (Just user) (ChatErrorAgent err $ Just connEntity) @@ -4098,6 +4103,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = let connectedIncognito = contactConnIncognito ct || incognitoMembership gInfo when (memberCategory m == GCPreMember) $ probeMatchingContactsAndMembers ct connectedIncognito True SENT msgId proxy -> do + void $ continueSending connEntity conn sentMsgDeliveryEvent conn msgId checkSndInlineFTComplete conn msgId ci_ <- withStore $ \db -> do @@ -4138,12 +4144,15 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = OK -> -- [async agent commands] continuation on receiving OK when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure () - MWARN msgId err -> + QCONT -> + void $ continueSending connEntity conn + MWARN msgId err -> do updateDirectItemStatus ct conn msgId (CISSndWarning $ agentSndError err) + processConnMWARN connEntity conn err MERR msgId err -> do updateDirectItemStatus ct conn msgId (CISSndError $ agentSndError err) toView $ CRChatError (Just user) (ChatErrorAgent err $ Just connEntity) - incAuthErrCounter connEntity conn err + processConnMERR connEntity conn err MERRS msgIds err -> do -- error cannot be AUTH error here updateDirectItemsStatus ct conn (L.toList msgIds) (CISSndError $ agentSndError err) @@ -4492,9 +4501,11 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = withAckMessage' "group rcvd" agentConnId msgMeta $ groupMsgReceived gInfo m conn msgMeta msgRcpt SENT msgId proxy -> do + continued <- continueSending connEntity conn sentMsgDeliveryEvent conn msgId checkSndInlineFTComplete conn msgId updateGroupItemStatus gInfo m conn msgId (CISSndSent SSPComplete) (Just $ isJust proxy) + when continued $ sendPendingGroupMessages user m conn SWITCH qd phase cStats -> do toView $ CRGroupMemberSwitch user gInfo m (SwitchProgress qd phase cStats) when (phase `elem` [SPStarted, SPCompleted]) $ case qd of @@ -4530,13 +4541,17 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = OK -> -- [async agent commands] continuation on receiving OK when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure () - MWARN msgId err -> + QCONT -> do + continued <- continueSending connEntity conn + when continued $ sendPendingGroupMessages user m conn + MWARN msgId err -> do withStore' $ \db -> updateGroupItemErrorStatus db msgId (groupMemberId' m) (CISSndWarning $ agentSndError err) + processConnMWARN connEntity conn err MERR msgId err -> do withStore' $ \db -> updateGroupItemErrorStatus db msgId (groupMemberId' m) (CISSndError $ agentSndError err) -- group errors are silenced to reduce load on UI event log -- toView $ CRChatError (Just user) (ChatErrorAgent err $ Just connEntity) - incAuthErrCounter connEntity conn err + processConnMERR connEntity conn err MERRS msgIds err -> do let newStatus = CISSndError $ agentSndError err -- error cannot be AUTH error here @@ -4666,7 +4681,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure () MERR _ err -> do toView $ CRChatError (Just user) (ChatErrorAgent err $ Just connEntity) - incAuthErrCounter connEntity conn err + processConnMERR connEntity conn err ERR err -> do toView $ CRChatError (Just user) (ChatErrorAgent err $ Just connEntity) when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure () @@ -4718,7 +4733,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = _ -> pure () MERR _ err -> do toView $ CRChatError (Just user) (ChatErrorAgent err $ Just connEntity) - incAuthErrCounter connEntity conn err + processConnMERR connEntity conn err ERR err -> do toView $ CRChatError (Just user) (ChatErrorAgent err $ Just connEntity) when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure () @@ -4758,17 +4773,40 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = | memberRole <= GRObserver = messageError "member is not allowed to send messages" | otherwise = a - incAuthErrCounter :: ConnectionEntity -> Connection -> AgentErrorType -> CM () - incAuthErrCounter connEntity conn err = do + processConnMERR :: ConnectionEntity -> Connection -> AgentErrorType -> CM () + processConnMERR connEntity conn err = do case err of SMP _ SMP.AUTH -> do - authErrCounter' <- withStore' $ \db -> incConnectionAuthErrCounter db user conn + authErrCounter' <- withStore' $ \db -> incAuthErrCounter db user conn when (authErrCounter' >= authErrDisableCount) $ case connEntity of RcvDirectMsgConnection ctConn (Just ct) -> do toView $ CRContactDisabled user ct {activeConn = Just ctConn {authErrCounter = authErrCounter'}} _ -> toView $ CRConnectionDisabled connEntity + SMP _ SMP.QUOTA -> + unless (connInactive conn) $ do + withStore' $ \db -> setQuotaErrCounter db user conn quotaErrSetOnMERR + toView $ CRConnectionInactive connEntity True _ -> pure () + processConnMWARN :: ConnectionEntity -> Connection -> AgentErrorType -> CM () + processConnMWARN connEntity conn err = do + case err of + SMP _ SMP.QUOTA -> + unless (connInactive conn) $ do + quotaErrCounter' <- withStore' $ \db -> incQuotaErrCounter db user conn + when (quotaErrCounter' >= quotaErrInactiveCount) $ + toView $ CRConnectionInactive connEntity True + _ -> pure () + + continueSending :: ConnectionEntity -> Connection -> CM Bool + continueSending connEntity conn = + if connInactive conn + then do + withStore' $ \db -> setQuotaErrCounter db user conn 0 + toView $ CRConnectionInactive connEntity False + pure True + else pure False + -- TODO v5.7 / v6.0 - together with deprecating old group protocol establishing direct connections? -- we could save command records only for agent APIs we process continuations for (INV) withCompletedCommand :: forall e. AEntityI e => Connection -> ACommand 'Agent e -> (CommandData -> CM ()) -> CM () @@ -6726,6 +6764,7 @@ memberSendAction chatMsgEvent members m@GroupMember {invitedByGroupMemberId} = c Nothing -> pendingOrForwarded Just conn@Connection {connStatus} | connDisabled conn || connStatus == ConnDeleted -> Nothing + | connInactive conn -> Just MSAPending | connStatus == ConnSndReady || connStatus == ConnReady -> Just (MSASend conn) | otherwise -> pendingOrForwarded where diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index 2c4c09c79c..c83044d564 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -752,6 +752,7 @@ data ChatResponse | CRAgentMsgCounts {msgCounts :: [(Text, (Int, Int))]} | CRContactDisabled {user :: User, contact :: Contact} | CRConnectionDisabled {connectionEntity :: ConnectionEntity} + | CRConnectionInactive {connectionEntity :: ConnectionEntity, inactive :: Bool} | CRAgentRcvQueueDeleted {agentConnId :: AgentConnId, server :: SMPServer, agentQueueId :: AgentQueueId, agentError_ :: Maybe AgentErrorType} | CRAgentConnDeleted {agentConnId :: AgentConnId} | CRAgentUserDeleted {agentUserId :: Int64} diff --git a/src/Simplex/Chat/Migrations/M20240528_quota_err_counter.hs b/src/Simplex/Chat/Migrations/M20240528_quota_err_counter.hs new file mode 100644 index 0000000000..ea1f3a78e7 --- /dev/null +++ b/src/Simplex/Chat/Migrations/M20240528_quota_err_counter.hs @@ -0,0 +1,18 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Migrations.M20240528_quota_err_counter where + +import Database.SQLite.Simple (Query) +import Database.SQLite.Simple.QQ (sql) + +m20240528_quota_err_counter :: Query +m20240528_quota_err_counter = + [sql| +ALTER TABLE connections ADD COLUMN quota_err_counter INTEGER NOT NULL DEFAULT 0; +|] + +down_m20240528_quota_err_counter :: Query +down_m20240528_quota_err_counter = + [sql| +ALTER TABLE connections DROP COLUMN quota_err_counter; +|] diff --git a/src/Simplex/Chat/Migrations/chat_schema.sql b/src/Simplex/Chat/Migrations/chat_schema.sql index 96d55badf9..fdbc44a9c3 100644 --- a/src/Simplex/Chat/Migrations/chat_schema.sql +++ b/src/Simplex/Chat/Migrations/chat_schema.sql @@ -289,6 +289,7 @@ CREATE TABLE connections( pq_encryption INTEGER NOT NULL DEFAULT 0, pq_snd_enabled INTEGER, pq_rcv_enabled INTEGER, + quota_err_counter INTEGER NOT NULL DEFAULT 0, FOREIGN KEY(snd_file_id, connection_id) REFERENCES snd_files(file_id, connection_id) ON DELETE CASCADE diff --git a/src/Simplex/Chat/Store/Connections.hs b/src/Simplex/Chat/Store/Connections.hs index 0d085d216c..2c7543f08a 100644 --- a/src/Simplex/Chat/Store/Connections.hs +++ b/src/Simplex/Chat/Store/Connections.hs @@ -85,7 +85,7 @@ getConnectionEntity db vr user@User {userId, userContactId} agentConnId = do [sql| SELECT connection_id, agent_conn_id, conn_level, via_contact, via_user_contact_link, via_group_link, group_link_id, custom_user_profile_id, conn_status, conn_type, contact_conn_initiated, local_alias, contact_id, group_member_id, snd_file_id, rcv_file_id, user_contact_link_id, - created_at, security_code, security_code_verified_at, pq_support, pq_encryption, pq_snd_enabled, pq_rcv_enabled, auth_err_counter, + created_at, security_code, security_code_verified_at, pq_support, pq_encryption, pq_snd_enabled, pq_rcv_enabled, auth_err_counter, quota_err_counter, conn_chat_version, peer_chat_min_version, peer_chat_max_version FROM connections WHERE user_id = ? AND agent_conn_id = ? diff --git a/src/Simplex/Chat/Store/Direct.hs b/src/Simplex/Chat/Store/Direct.hs index afbd3f7960..1145c2494b 100644 --- a/src/Simplex/Chat/Store/Direct.hs +++ b/src/Simplex/Chat/Store/Direct.hs @@ -51,8 +51,10 @@ module Simplex.Chat.Store.Direct updateContactStatus, updateGroupUnreadChat, setConnectionVerified, - incConnectionAuthErrCounter, - setConnectionAuthErrCounter, + incAuthErrCounter, + setAuthErrCounter, + incQuotaErrCounter, + setQuotaErrCounter, getUserContacts, createOrUpdateContactRequest, getContactRequest', @@ -183,7 +185,7 @@ getContactByConnReqHash db vr user@User {userId} cReqHash = cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.contact_group_member_id, ct.contact_grp_inv_sent, ct.ui_themes, ct.chat_deleted, ct.custom_data, -- Connection c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, - c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, + c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter, c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version FROM contacts ct JOIN contact_profiles cp ON ct.contact_profile_id = cp.contact_profile_id @@ -473,19 +475,32 @@ setConnectionVerified db User {userId} connId code = do updatedAt <- getCurrentTime DB.execute db "UPDATE connections SET security_code = ?, security_code_verified_at = ?, updated_at = ? WHERE user_id = ? AND connection_id = ?" (code, code $> updatedAt, updatedAt, userId, connId) -incConnectionAuthErrCounter :: DB.Connection -> User -> Connection -> IO Int -incConnectionAuthErrCounter db User {userId} Connection {connId, authErrCounter} = do +incAuthErrCounter :: DB.Connection -> User -> Connection -> IO Int +incAuthErrCounter db User {userId} Connection {connId, authErrCounter} = do updatedAt <- getCurrentTime (counter_ :: Maybe Int) <- maybeFirstRow fromOnly $ DB.query db "SELECT auth_err_counter FROM connections WHERE user_id = ? AND connection_id = ?" (userId, connId) let counter' = fromMaybe authErrCounter counter_ + 1 DB.execute db "UPDATE connections SET auth_err_counter = ?, updated_at = ? WHERE user_id = ? AND connection_id = ?" (counter', updatedAt, userId, connId) pure counter' -setConnectionAuthErrCounter :: DB.Connection -> User -> Connection -> Int -> IO () -setConnectionAuthErrCounter db User {userId} Connection {connId} counter = do +setAuthErrCounter :: DB.Connection -> User -> Connection -> Int -> IO () +setAuthErrCounter db User {userId} Connection {connId} counter = do updatedAt <- getCurrentTime DB.execute db "UPDATE connections SET auth_err_counter = ?, updated_at = ? WHERE user_id = ? AND connection_id = ?" (counter, updatedAt, userId, connId) +incQuotaErrCounter :: DB.Connection -> User -> Connection -> IO Int +incQuotaErrCounter db User {userId} Connection {connId, quotaErrCounter} = do + updatedAt <- getCurrentTime + (counter_ :: Maybe Int) <- maybeFirstRow fromOnly $ DB.query db "SELECT quota_err_counter FROM connections WHERE user_id = ? AND connection_id = ?" (userId, connId) + let counter' = fromMaybe quotaErrCounter counter_ + 1 + DB.execute db "UPDATE connections SET quota_err_counter = ?, updated_at = ? WHERE user_id = ? AND connection_id = ?" (counter', updatedAt, userId, connId) + pure counter' + +setQuotaErrCounter :: DB.Connection -> User -> Connection -> Int -> IO () +setQuotaErrCounter db User {userId} Connection {connId} counter = do + updatedAt <- getCurrentTime + DB.execute db "UPDATE connections SET quota_err_counter = ?, updated_at = ? WHERE user_id = ? AND connection_id = ?" (counter, updatedAt, userId, connId) + updateContactProfile_ :: DB.Connection -> UserId -> ProfileId -> Profile -> IO () updateContactProfile_ db userId profileId profile = do currentTs <- getCurrentTime @@ -609,7 +624,7 @@ createOrUpdateContactRequest db vr user@User {userId} userContactLinkId invId (V cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.contact_group_member_id, ct.contact_grp_inv_sent, ct.ui_themes, ct.chat_deleted, ct.custom_data, -- Connection c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, - c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, + c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter, c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version FROM contacts ct JOIN contact_profiles cp ON ct.contact_profile_id = cp.contact_profile_id @@ -794,7 +809,7 @@ getContact_ db vr user@User {userId} contactId deleted = cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.contact_group_member_id, ct.contact_grp_inv_sent, ct.ui_themes, ct.chat_deleted, ct.custom_data, -- Connection c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, - c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, + c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter, c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version FROM contacts ct JOIN contact_profiles cp ON ct.contact_profile_id = cp.contact_profile_id @@ -848,7 +863,7 @@ getContactConnections db vr userId Contact {contactId} = [sql| SELECT c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, - c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, + c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter, c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version FROM connections c JOIN contacts ct ON ct.contact_id = c.contact_id @@ -866,7 +881,7 @@ getConnectionById db vr User {userId} connId = ExceptT $ do [sql| SELECT connection_id, agent_conn_id, conn_level, via_contact, via_user_contact_link, via_group_link, group_link_id, custom_user_profile_id, conn_status, conn_type, contact_conn_initiated, local_alias, contact_id, group_member_id, snd_file_id, rcv_file_id, user_contact_link_id, - created_at, security_code, security_code_verified_at, pq_support, pq_encryption, pq_snd_enabled, pq_rcv_enabled, auth_err_counter, + created_at, security_code, security_code_verified_at, pq_support, pq_encryption, pq_snd_enabled, pq_rcv_enabled, auth_err_counter, quota_err_counter, conn_chat_version, peer_chat_min_version, peer_chat_max_version FROM connections WHERE user_id = ? AND connection_id = ? diff --git a/src/Simplex/Chat/Store/Groups.hs b/src/Simplex/Chat/Store/Groups.hs index 21b50113b0..42637d4169 100644 --- a/src/Simplex/Chat/Store/Groups.hs +++ b/src/Simplex/Chat/Store/Groups.hs @@ -201,7 +201,7 @@ getGroupLinkConnection db vr User {userId} groupInfo@GroupInfo {groupId} = [sql| SELECT c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, - c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, + c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter, c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version FROM connections c JOIN user_contact_links uc ON c.user_contact_link_id = uc.user_contact_link_id @@ -287,7 +287,7 @@ getGroupAndMember db User {userId, userContactId} groupMemberId vr = m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, p.local_alias, p.preferences, c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, - c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, + c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter, c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version FROM group_members m JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id) @@ -705,7 +705,7 @@ groupMemberQuery = m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, p.local_alias, p.preferences, c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, - c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, + c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter, c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version FROM group_members m JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id) @@ -1326,7 +1326,7 @@ getViaGroupMember db vr User {userId, userContactId} Contact {contactId} = m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, p.local_alias, p.preferences, c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, - c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, + c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter, c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version FROM group_members m JOIN contacts ct ON ct.contact_id = m.contact_id @@ -1970,7 +1970,8 @@ createMemberContact pqEncryption = PQEncOff, pqSndEnabled = Nothing, pqRcvEnabled = Nothing, - authErrCounter = 0 + authErrCounter = 0, + quotaErrCounter = 0 } mergedPreferences = contactUserPreferences user userPreferences preferences $ connIncognito ctConn pure Contact {contactId, localDisplayName, profile = memberProfile, activeConn = Just ctConn, viaGroup = Nothing, contactUsed = True, contactStatus = CSActive, chatSettings = defaultChatSettings, userPreferences, mergedPreferences, createdAt = currentTs, updatedAt = currentTs, chatTs = Just currentTs, contactGroupMemberId = Just groupMemberId, contactGrpInvSent = False, uiThemes = Nothing, chatDeleted = False, customData = Nothing} @@ -2103,7 +2104,8 @@ createMemberContactConn_ pqEncryption = PQEncOff, pqSndEnabled = Nothing, pqRcvEnabled = Nothing, - authErrCounter = 0 + authErrCounter = 0, + quotaErrCounter = 0 } updateMemberProfile :: DB.Connection -> User -> GroupMember -> Profile -> ExceptT StoreError IO GroupMember diff --git a/src/Simplex/Chat/Store/Messages.hs b/src/Simplex/Chat/Store/Messages.hs index 0487b80c17..494e5635f4 100644 --- a/src/Simplex/Chat/Store/Messages.hs +++ b/src/Simplex/Chat/Store/Messages.hs @@ -295,7 +295,7 @@ getPendingGroupMessages db groupMemberId = FROM pending_group_messages pgm JOIN messages m USING (message_id) WHERE pgm.group_member_id = ? - ORDER BY pgm.message_id ASC + ORDER BY pgm.created_at ASC, pgm.message_id ASC |] (Only groupMemberId) where diff --git a/src/Simplex/Chat/Store/Migrations.hs b/src/Simplex/Chat/Store/Migrations.hs index a79a31f75d..5c9082b361 100644 --- a/src/Simplex/Chat/Store/Migrations.hs +++ b/src/Simplex/Chat/Store/Migrations.hs @@ -109,6 +109,7 @@ import Simplex.Chat.Migrations.M20240430_ui_theme import Simplex.Chat.Migrations.M20240501_chat_deleted import Simplex.Chat.Migrations.M20240510_chat_items_via_proxy import Simplex.Chat.Migrations.M20240515_rcv_files_user_approved_relays +import Simplex.Chat.Migrations.M20240528_quota_err_counter import Simplex.Messaging.Agent.Store.SQLite.Migrations (Migration (..)) schemaMigrations :: [(String, Query, Maybe Query)] @@ -217,7 +218,8 @@ schemaMigrations = ("20240430_ui_theme", m20240430_ui_theme, Just down_m20240430_ui_theme), ("20240501_chat_deleted", m20240501_chat_deleted, Just down_m20240501_chat_deleted), ("20240510_chat_items_via_proxy", m20240510_chat_items_via_proxy, Just down_m20240510_chat_items_via_proxy), - ("20240515_rcv_files_user_approved_relays", m20240515_rcv_files_user_approved_relays, Just down_m20240515_rcv_files_user_approved_relays) + ("20240515_rcv_files_user_approved_relays", m20240515_rcv_files_user_approved_relays, Just down_m20240515_rcv_files_user_approved_relays), + ("20240528_quota_err_counter", m20240528_quota_err_counter, Just down_m20240528_quota_err_counter) ] -- | The list of migrations in ascending order by date diff --git a/src/Simplex/Chat/Store/Profiles.hs b/src/Simplex/Chat/Store/Profiles.hs index 9b83e7299f..be06ea8878 100644 --- a/src/Simplex/Chat/Store/Profiles.hs +++ b/src/Simplex/Chat/Store/Profiles.hs @@ -342,7 +342,7 @@ getUserAddressConnections db vr User {userId} = do [sql| SELECT c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, - c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, + c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter, c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version FROM connections c JOIN user_contact_links uc ON c.user_contact_link_id = uc.user_contact_link_id @@ -358,7 +358,7 @@ getUserContactLinks db vr User {userId} = [sql| SELECT c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, - c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, + c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter, c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version, uc.user_contact_link_id, uc.conn_req_contact, uc.group_id FROM connections c diff --git a/src/Simplex/Chat/Store/Shared.hs b/src/Simplex/Chat/Store/Shared.hs index 113a84966d..c364cf10c2 100644 --- a/src/Simplex/Chat/Store/Shared.hs +++ b/src/Simplex/Chat/Store/Shared.hs @@ -163,12 +163,12 @@ toFileInfo (fileId, fileStatus, filePath) = CIFileInfo {fileId, fileStatus, file type EntityIdsRow = (Maybe Int64, Maybe Int64, Maybe Int64, Maybe Int64, Maybe Int64) -type ConnectionRow = (Int64, ConnId, Int, Maybe Int64, Maybe Int64, Bool, Maybe GroupLinkId, Maybe Int64, ConnStatus, ConnType, Bool, LocalAlias) :. EntityIdsRow :. (UTCTime, Maybe Text, Maybe UTCTime, PQSupport, PQEncryption, Maybe PQEncryption, Maybe PQEncryption, Int, Maybe VersionChat, VersionChat, VersionChat) +type ConnectionRow = (Int64, ConnId, Int, Maybe Int64, Maybe Int64, Bool, Maybe GroupLinkId, Maybe Int64, ConnStatus, ConnType, Bool, LocalAlias) :. EntityIdsRow :. (UTCTime, Maybe Text, Maybe UTCTime, PQSupport, PQEncryption, Maybe PQEncryption, Maybe PQEncryption, Int, Int, Maybe VersionChat, VersionChat, VersionChat) -type MaybeConnectionRow = (Maybe Int64, Maybe ConnId, Maybe Int, Maybe Int64, Maybe Int64, Maybe Bool, Maybe GroupLinkId, Maybe Int64, Maybe ConnStatus, Maybe ConnType, Maybe Bool, Maybe LocalAlias) :. EntityIdsRow :. (Maybe UTCTime, Maybe Text, Maybe UTCTime, Maybe PQSupport, Maybe PQEncryption, Maybe PQEncryption, Maybe PQEncryption, Maybe Int, Maybe VersionChat, Maybe VersionChat, Maybe VersionChat) +type MaybeConnectionRow = (Maybe Int64, Maybe ConnId, Maybe Int, Maybe Int64, Maybe Int64, Maybe Bool, Maybe GroupLinkId, Maybe Int64, Maybe ConnStatus, Maybe ConnType, Maybe Bool, Maybe LocalAlias) :. EntityIdsRow :. (Maybe UTCTime, Maybe Text, Maybe UTCTime, Maybe PQSupport, Maybe PQEncryption, Maybe PQEncryption, Maybe PQEncryption, Maybe Int, Maybe Int, Maybe VersionChat, Maybe VersionChat, Maybe VersionChat) toConnection :: VersionRangeChat -> ConnectionRow -> Connection -toConnection vr ((connId, acId, connLevel, viaContact, viaUserContactLink, viaGroupLink, groupLinkId, customUserProfileId, connStatus, connType, contactConnInitiated, localAlias) :. (contactId, groupMemberId, sndFileId, rcvFileId, userContactLinkId) :. (createdAt, code_, verifiedAt_, pqSupport, pqEncryption, pqSndEnabled, pqRcvEnabled, authErrCounter, chatV, minVer, maxVer)) = +toConnection vr ((connId, acId, connLevel, viaContact, viaUserContactLink, viaGroupLink, groupLinkId, customUserProfileId, connStatus, connType, contactConnInitiated, localAlias) :. (contactId, groupMemberId, sndFileId, rcvFileId, userContactLinkId) :. (createdAt, code_, verifiedAt_, pqSupport, pqEncryption, pqSndEnabled, pqRcvEnabled, authErrCounter, quotaErrCounter, chatV, minVer, maxVer)) = Connection { connId, agentConnId = AgentConnId acId, @@ -191,6 +191,7 @@ toConnection vr ((connId, acId, connLevel, viaContact, viaUserContactLink, viaGr pqSndEnabled, pqRcvEnabled, authErrCounter, + quotaErrCounter, createdAt } where @@ -203,8 +204,8 @@ toConnection vr ((connId, acId, connLevel, viaContact, viaUserContactLink, viaGr entityId_ ConnUserContact = userContactLinkId toMaybeConnection :: VersionRangeChat -> MaybeConnectionRow -> Maybe Connection -toMaybeConnection vr ((Just connId, Just agentConnId, Just connLevel, viaContact, viaUserContactLink, Just viaGroupLink, groupLinkId, customUserProfileId, Just connStatus, Just connType, Just contactConnInitiated, Just localAlias) :. (contactId, groupMemberId, sndFileId, rcvFileId, userContactLinkId) :. (Just createdAt, code_, verifiedAt_, Just pqSupport, Just pqEncryption, pqSndEnabled_, pqRcvEnabled_, Just authErrCounter, connChatVersion, Just minVer, Just maxVer)) = - Just $ toConnection vr ((connId, agentConnId, connLevel, viaContact, viaUserContactLink, viaGroupLink, groupLinkId, customUserProfileId, connStatus, connType, contactConnInitiated, localAlias) :. (contactId, groupMemberId, sndFileId, rcvFileId, userContactLinkId) :. (createdAt, code_, verifiedAt_, pqSupport, pqEncryption, pqSndEnabled_, pqRcvEnabled_, authErrCounter, connChatVersion, minVer, maxVer)) +toMaybeConnection vr ((Just connId, Just agentConnId, Just connLevel, viaContact, viaUserContactLink, Just viaGroupLink, groupLinkId, customUserProfileId, Just connStatus, Just connType, Just contactConnInitiated, Just localAlias) :. (contactId, groupMemberId, sndFileId, rcvFileId, userContactLinkId) :. (Just createdAt, code_, verifiedAt_, Just pqSupport, Just pqEncryption, pqSndEnabled_, pqRcvEnabled_, Just authErrCounter, Just quotaErrCounter, connChatVersion, Just minVer, Just maxVer)) = + Just $ toConnection vr ((connId, agentConnId, connLevel, viaContact, viaUserContactLink, viaGroupLink, groupLinkId, customUserProfileId, connStatus, connType, contactConnInitiated, localAlias) :. (contactId, groupMemberId, sndFileId, rcvFileId, userContactLinkId) :. (createdAt, code_, verifiedAt_, pqSupport, pqEncryption, pqSndEnabled_, pqRcvEnabled_, authErrCounter, quotaErrCounter, connChatVersion, minVer, maxVer)) toMaybeConnection _ _ = Nothing createConnection_ :: DB.Connection -> UserId -> ConnType -> Maybe Int64 -> ConnId -> VersionChat -> VersionRangeChat -> Maybe ContactId -> Maybe Int64 -> Maybe ProfileId -> Int -> UTCTime -> SubscriptionMode -> PQSupport -> IO Connection @@ -249,7 +250,8 @@ createConnection_ db userId connType entityId acId connChatVersion peerChatVRang pqEncryption = CR.pqSupportToEnc pqSup, pqSndEnabled = Nothing, pqRcvEnabled = Nothing, - authErrCounter = 0 + authErrCounter = 0, + quotaErrCounter = 0 } where ent ct = if connType == ct then entityId else Nothing diff --git a/src/Simplex/Chat/Types.hs b/src/Simplex/Chat/Types.hs index ca6cd2e375..047a9c68f7 100644 --- a/src/Simplex/Chat/Types.hs +++ b/src/Simplex/Chat/Types.hs @@ -1303,6 +1303,7 @@ data Connection = Connection pqSndEnabled :: Maybe PQEncryption, pqRcvEnabled :: Maybe PQEncryption, authErrCounter :: Int, + quotaErrCounter :: Int, -- if exceeds limit messages to group members are created as pending; sending to contacts is unaffected by this createdAt :: UTCTime } deriving (Eq, Show) @@ -1316,6 +1317,15 @@ authErrDisableCount = 10 connDisabled :: Connection -> Bool connDisabled Connection {authErrCounter} = authErrCounter >= authErrDisableCount +quotaErrInactiveCount :: Int +quotaErrInactiveCount = 5 + +quotaErrSetOnMERR :: Int +quotaErrSetOnMERR = 999 + +connInactive :: Connection -> Bool +connInactive Connection {quotaErrCounter} = quotaErrCounter >= quotaErrInactiveCount + data SecurityCode = SecurityCode {securityCode :: Text, verifiedAt :: UTCTime} deriving (Eq, Show) diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs index 671706c5f2..90928c7f88 100644 --- a/src/Simplex/Chat/View.hs +++ b/src/Simplex/Chat/View.hs @@ -378,6 +378,7 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe CRAgentMsgCounts {msgCounts} -> ["received messages (total, duplicates):", plain . LB.unpack $ J.encode msgCounts] CRContactDisabled u c -> ttyUser u ["[" <> ttyContact' c <> "] connection is disabled, to enable: " <> highlight ("/enable " <> viewContactName c) <> ", to delete: " <> highlight ("/d " <> viewContactName c)] CRConnectionDisabled entity -> viewConnectionEntityDisabled entity + CRConnectionInactive entity inactive -> viewConnectionEntityInactive entity inactive CRAgentRcvQueueDeleted acId srv aqId err_ -> [ ("completed deleting rcv queue, agent connection id: " <> sShow acId) <> (", server: " <> sShow srv) @@ -2077,6 +2078,11 @@ viewConnectionEntityDisabled entity = case entity of where entityLabel = connEntityLabel entity +viewConnectionEntityInactive :: ConnectionEntity -> Bool -> [StyledString] +viewConnectionEntityInactive entity inactive + | inactive = ["[" <> connEntityLabel entity <> "] connection is marked as inactive"] + | otherwise = ["[" <> connEntityLabel entity <> "] inactive connection is marked as active"] + connEntityLabel :: ConnectionEntity -> StyledString connEntityLabel = \case RcvDirectMsgConnection _ (Just Contact {localDisplayName = c}) -> plain c diff --git a/tests/ChatClient.hs b/tests/ChatClient.hs index 83ac69ebe9..8f7bd1dc4e 100644 --- a/tests/ChatClient.hs +++ b/tests/ChatClient.hs @@ -401,8 +401,8 @@ testChatCfg4 cfg p1 p2 p3 p4 test = testChatN cfg testOpts [p1, p2, p3, p4] test concurrentlyN_ :: [IO a] -> IO () concurrentlyN_ = mapConcurrently_ id -serverCfg :: ServerConfig -serverCfg = +smpServerCfg :: ServerConfig +smpServerCfg = ServerConfig { transports = [(serverPort, transport @TLS)], tbqSize = 1, @@ -436,7 +436,10 @@ serverCfg = } withSmpServer :: IO () -> IO () -withSmpServer = serverBracket (`runSMPServerBlocking` serverCfg) +withSmpServer = withSmpServer' smpServerCfg + +withSmpServer' :: ServerConfig -> IO () -> IO () +withSmpServer' cfg = serverBracket (`runSMPServerBlocking` cfg) xftpTestPort :: ServiceName xftpTestPort = "7002" diff --git a/tests/ChatTests/Groups.hs b/tests/ChatTests/Groups.hs index 8cd853ade0..009a26cf0b 100644 --- a/tests/ChatTests/Groups.hs +++ b/tests/ChatTests/Groups.hs @@ -1,6 +1,9 @@ +{-# LANGUAGE NumericUnderscores #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE PatternSynonyms #-} {-# LANGUAGE PostfixOperators #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE TypeApplications #-} module ChatTests.Groups where @@ -13,11 +16,16 @@ import qualified Data.ByteString.Char8 as B import Data.List (isInfixOf) import qualified Data.Text as T import Simplex.Chat.Controller (ChatConfig (..)) +import Simplex.Chat.Options import Simplex.Chat.Protocol (supportedChatVRange) import Simplex.Chat.Store (agentStoreFile, chatStoreFile) import Simplex.Chat.Types (VersionRangeChat) import Simplex.Chat.Types.Shared (GroupMemberRole (..)) +import Simplex.Messaging.Agent.Env.SQLite +import Simplex.Messaging.Agent.RetryInterval import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB +import Simplex.Messaging.Server.Env.STM hiding (subscriptions) +import Simplex.Messaging.Transport import System.Directory (copyFile) import System.FilePath (()) import Test.Hspec hiding (it) @@ -150,6 +158,8 @@ chatGroupTests = do it "another admin can unblock" testBlockForAllAnotherAdminUnblocks it "member was blocked before joining group" testBlockForAllBeforeJoining it "can't repeat block, unblock" testBlockForAllCantRepeat + describe "group member inactivity" $ do + it "mark member inactive on reaching quota" testGroupMemberInactive where _0 = supportedChatVRange -- don't create direct connections _1 = groupCreateDirectVRange @@ -6072,3 +6082,73 @@ testBlockForAllCantRepeat = [alice, cath] *<# "#team bob> 3" bob #$> ("/_get chat #1 count=3", chat, [(1, "1"), (1, "2"), (1, "3")]) + +testGroupMemberInactive :: HasCallStack => FilePath -> IO () +testGroupMemberInactive tmp = do + withSmpServer' serverCfg' $ do + withNewTestChatCfgOpts tmp cfg' opts' "alice" aliceProfile $ \alice -> do + withNewTestChatCfgOpts tmp cfg' opts' "bob" bobProfile $ \bob -> do + createGroup2 "team" alice bob + + alice #> "#team hi" + bob <# "#team alice> hi" + bob #> "#team hey" + alice <# "#team bob> hey" + + -- bob is offline + alice #> "#team 1" + alice #> "#team 2" + alice #> "#team 3" + alice <## "[#team bob] connection is marked as inactive" + -- 4 and 5 will be sent to bob as pending messages + alice #> "#team 4" + alice #> "#team 5" + + pgmCount <- withCCTransaction alice $ \db -> + DB.query_ db "SELECT count(1) FROM pending_group_messages" :: IO [[Int]] + pgmCount `shouldBe` [[2]] + + threadDelay 1500000 + + withTestChatCfgOpts tmp cfg' opts' "bob" $ \bob -> do + bob <## "1 contacts connected (use /cs for the list)" + bob <## "#team: connected to server(s)" + bob <# "#team alice> 1" + bob <# "#team alice> 2" + bob <#. "#team alice> skipped message ID" + alice <## "[#team bob] inactive connection is marked as active" + + bob <# "#team alice> 4" + bob <# "#team alice> 5" + + pgmCount' <- withCCTransaction alice $ \db -> + DB.query_ db "SELECT count(1) FROM pending_group_messages" :: IO [[Int]] + pgmCount' `shouldBe` [[0]] + + -- delivery works + alice #> "#team hi" + bob <# "#team alice> hi" + bob #> "#team hey" + alice <# "#team bob> hey" + where + serverCfg' = + smpServerCfg + { transports = [("7003", transport @TLS)], + msgQueueQuota = 2 + } + fastRetryInterval = defaultReconnectInterval {initialInterval = 50_000} -- same as in agent tests + cfg' = + testCfg + { agentConfig = + testAgentCfg + { quotaExceededTimeout = 1, + messageRetryInterval = RetryInterval2 {riFast = fastRetryInterval, riSlow = fastRetryInterval} + } + } + opts' = + testOpts + { coreOptions = + testCoreOpts + { smpServers = ["smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7003"] + } + }