From ef28215284db2bcff949706ac99cbde610fcea21 Mon Sep 17 00:00:00 2001 From: JRoberts <8711996+jr-simplex@users.noreply.github.com> Date: Sat, 1 Oct 2022 14:31:21 +0400 Subject: [PATCH] core: fully delete group chat items instead of overwriting content (#1154) --- simplex-chat.cabal | 1 + src/Simplex/Chat.hs | 59 +++++---- src/Simplex/Chat/Migrations/M20220122_v1_1.hs | 2 +- .../M20221001_shared_msg_id_indices.hs | 25 ++++ src/Simplex/Chat/Migrations/chat_schema.sql | 20 ++-- src/Simplex/Chat/Store.hs | 112 +++++++----------- tests/ChatTests.hs | 54 +++++---- 7 files changed, 149 insertions(+), 124 deletions(-) create mode 100644 src/Simplex/Chat/Migrations/M20221001_shared_msg_id_indices.hs diff --git a/simplex-chat.cabal b/simplex-chat.cabal index 0e32a1c029..d599377fdc 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -51,6 +51,7 @@ library Simplex.Chat.Migrations.M20220909_commands Simplex.Chat.Migrations.M20220926_connection_alias Simplex.Chat.Migrations.M20220928_settings + Simplex.Chat.Migrations.M20221001_shared_msg_id_indices Simplex.Chat.Mobile Simplex.Chat.Options Simplex.Chat.ProfileGenerator diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index 7c80c7e1e4..aadf6e1fb6 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -434,12 +434,12 @@ processChatCommand = \case case (mode, msgDir, itemSharedMsgId) of (CIDMInternal, _, _) -> do deleteCIFile user file - toCi <- withStore $ \db -> deleteGroupChatItemInternal db user gInfo itemId + toCi <- withStore $ \db -> deleteGroupChatItemLocal db user gInfo itemId CIDMInternal pure $ CRChatItemDeleted (AChatItem SCTGroup msgDir (GroupChat gInfo) deletedItem) toCi (CIDMBroadcast, SMDSnd, Just itemSharedMId) -> do - SndMessage {msgId} <- sendGroupMessage gInfo ms (XMsgDel itemSharedMId) + void $ sendGroupMessage gInfo ms (XMsgDel itemSharedMId) deleteCIFile user file - toCi <- withStore $ \db -> deleteGroupChatItemSndBroadcast db user gInfo itemId msgId + toCi <- withStore $ \db -> deleteGroupChatItemLocal db user gInfo itemId CIDMBroadcast setActive $ ActiveG gName pure $ CRChatItemDeleted (AChatItem SCTGroup msgDir (GroupChat gInfo) deletedItem) toCi (CIDMBroadcast, _, _) -> throwChatError CEInvalidChatItemDelete @@ -1112,7 +1112,7 @@ deleteGroupChatItem user gInfo (itemId, fileInfo_) = do forM_ fileInfo_ $ \fileInfo -> do cancelFile user fileInfo `catchError` \_ -> pure () withFilesFolder $ \filesFolder -> deleteFile filesFolder fileInfo - void $ withStore $ \db -> deleteGroupChatItemInternal db user gInfo itemId + void $ withStore $ \db -> deleteGroupChatItemLocal db user gInfo itemId CIDMInternal -- perform an action only if filesFolder is set (i.e. on mobile devices) withFilesFolder :: ChatMonad m => (FilePath -> m ()) -> m () @@ -1688,7 +1688,7 @@ processAgentMessage (Just user@User {userId, profile}) corrId agentConnId agentM withAckMessage agentConnId cmdId msgMeta $ case chatMsgEvent of XMsgNew mc -> newGroupContentMessage gInfo m mc msg msgMeta - XMsgUpdate sharedMsgId mContent -> groupMessageUpdate gInfo m sharedMsgId mContent msg + XMsgUpdate sharedMsgId mContent -> groupMessageUpdate gInfo m sharedMsgId mContent msg msgMeta XMsgDel sharedMsgId -> groupMessageDelete gInfo m sharedMsgId msg -- TODO discontinue XFile XFile fInv -> processGroupFileInvitation' gInfo m fInv msg msgMeta @@ -2001,30 +2001,41 @@ processAgentMessage (Just user@User {userId, profile}) corrId agentConnId agentM when (enableNtfs chatSettings) $ showMsgToast ("#" <> g <> " " <> c <> "> ") content formattedText setActive $ ActiveG g - groupMessageUpdate :: GroupInfo -> GroupMember -> SharedMsgId -> MsgContent -> RcvMessage -> m () - groupMessageUpdate gInfo@GroupInfo {groupId} GroupMember {memberId} sharedMsgId mc RcvMessage {msgId} = do - CChatItem msgDir ChatItem {chatDir, meta = CIMeta {itemId}} <- withStore $ \db -> getGroupChatItemBySharedMsgId db user groupId sharedMsgId - case (msgDir, chatDir) of - (SMDRcv, CIGroupRcv m) -> - if sameMemberId memberId m - then do - updCi <- withStore $ \db -> updateGroupChatItem db user groupId itemId (CIRcvMsgContent mc) msgId - toView . CRChatItemUpdated $ AChatItem SCTGroup SMDRcv (GroupChat gInfo) updCi - let g = groupName' gInfo - setActive $ ActiveG g - else messageError "x.msg.update: group member attempted to update a message of another member" - (SMDSnd, _) -> messageError "x.msg.update: group member attempted invalid message update" + groupMessageUpdate :: GroupInfo -> GroupMember -> SharedMsgId -> MsgContent -> RcvMessage -> MsgMeta -> m () + groupMessageUpdate gInfo@GroupInfo {groupId, localDisplayName = g} m@GroupMember {groupMemberId, memberId} sharedMsgId mc msg@RcvMessage {msgId} msgMeta = + updateRcvChatItem `catchError` \e -> + case e of + (ChatErrorStore (SEChatItemSharedMsgIdNotFound _)) -> do + -- This patches initial sharedMsgId into chat item when locally deleted chat item + -- received an update from the sender, so that it can be referenced later (e.g. by broadcast delete). + -- Chat item and update message which created it will have different sharedMsgId in this case... + ci <- saveRcvChatItem' user (CDGroupRcv gInfo m) msg (Just sharedMsgId) msgMeta (CIRcvMsgContent mc) Nothing + toView . CRChatItemUpdated $ AChatItem SCTGroup SMDRcv (GroupChat gInfo) ci + setActive $ ActiveG g + _ -> throwError e + where + updateRcvChatItem = do + CChatItem msgDir ChatItem {chatDir, meta = CIMeta {itemId}} <- withStore $ \db -> getGroupChatItemBySharedMsgId db user groupId groupMemberId sharedMsgId + case (msgDir, chatDir) of + (SMDRcv, CIGroupRcv m') -> + if sameMemberId memberId m' + then do + updCi <- withStore $ \db -> updateGroupChatItem db user groupId itemId (CIRcvMsgContent mc) msgId + toView . CRChatItemUpdated $ AChatItem SCTGroup SMDRcv (GroupChat gInfo) updCi + setActive $ ActiveG g + else messageError "x.msg.update: group member attempted to update a message of another member" -- shouldn't happen now that query includes group member id + (SMDSnd, _) -> messageError "x.msg.update: group member attempted invalid message update" groupMessageDelete :: GroupInfo -> GroupMember -> SharedMsgId -> RcvMessage -> m () - groupMessageDelete gInfo@GroupInfo {groupId} GroupMember {memberId} sharedMsgId RcvMessage {msgId} = do - CChatItem msgDir deletedItem@ChatItem {chatDir, meta = CIMeta {itemId}} <- withStore $ \db -> getGroupChatItemBySharedMsgId db user groupId sharedMsgId + groupMessageDelete gInfo@GroupInfo {groupId} GroupMember {groupMemberId, memberId} sharedMsgId RcvMessage {msgId} = do + CChatItem msgDir deletedItem@ChatItem {chatDir, meta = CIMeta {itemId}} <- withStore $ \db -> getGroupChatItemBySharedMsgId db user groupId groupMemberId sharedMsgId case (msgDir, chatDir) of (SMDRcv, CIGroupRcv m) -> if sameMemberId memberId m then do toCi <- withStore $ \db -> deleteGroupChatItemRcvBroadcast db user gInfo itemId msgId toView $ CRChatItemDeleted (AChatItem SCTGroup SMDRcv (GroupChat gInfo) deletedItem) toCi - else messageError "x.msg.del: group member attempted to delete a message of another member" + else messageError "x.msg.del: group member attempted to delete a message of another member" -- shouldn't happen now that query includes group member id (SMDSnd, _) -> messageError "x.msg.del: group member attempted invalid message delete" -- TODO remove once XFile is discontinued @@ -2073,10 +2084,10 @@ processAgentMessage (Just user@User {userId, profile}) corrId agentConnId agentM else messageError "x.file.acpt.inv: fileName is different from expected" xFileCancelGroup :: GroupInfo -> GroupMember -> SharedMsgId -> MsgMeta -> m () - xFileCancelGroup g@GroupInfo {groupId} mem@GroupMember {memberId} sharedMsgId msgMeta = do + xFileCancelGroup g@GroupInfo {groupId} mem@GroupMember {groupMemberId, memberId} sharedMsgId msgMeta = do checkIntegrityCreateItem (CDGroupRcv g mem) msgMeta fileId <- withStore $ \db -> getGroupFileIdBySharedMsgId db userId groupId sharedMsgId - CChatItem msgDir ChatItem {chatDir} <- withStore $ \db -> getGroupChatItemBySharedMsgId db user groupId sharedMsgId + CChatItem msgDir ChatItem {chatDir} <- withStore $ \db -> getGroupChatItemBySharedMsgId db user groupId groupMemberId sharedMsgId case (msgDir, chatDir) of (SMDRcv, CIGroupRcv m) -> do if sameMemberId memberId m @@ -2085,7 +2096,7 @@ processAgentMessage (Just user@User {userId, profile}) corrId agentConnId agentM unless cancelled $ do cancelRcvFileTransfer user ft toView $ CRRcvFileSndCancelled ft - else messageError "x.file.cancel: group member attempted to cancel file of another member" + else messageError "x.file.cancel: group member attempted to cancel file of another member" -- shouldn't happen now that query includes group member id (SMDSnd, _) -> messageError "x.file.cancel: group member attempted invalid file cancel" xFileAcptInvGroup :: GroupInfo -> GroupMember -> SharedMsgId -> ConnReqInvitation -> String -> MsgMeta -> m () diff --git a/src/Simplex/Chat/Migrations/M20220122_v1_1.hs b/src/Simplex/Chat/Migrations/M20220122_v1_1.hs index 3e421b631b..deefc19ee2 100644 --- a/src/Simplex/Chat/Migrations/M20220122_v1_1.hs +++ b/src/Simplex/Chat/Migrations/M20220122_v1_1.hs @@ -33,7 +33,7 @@ CREATE TABLE chat_items ( created_by_msg_id INTEGER UNIQUE REFERENCES messages (message_id) ON DELETE SET NULL, item_sent INTEGER NOT NULL, -- 0 for received, 1 for sent item_ts TEXT NOT NULL, -- broker_ts of creating message for received, created_at for sent - item_deleted INTEGER NOT NULL DEFAULT 0, -- 1 for deleted, + item_deleted INTEGER NOT NULL DEFAULT 0, -- 1 for deleted, -- ! legacy field that was used for group chat items when they weren't fully deleted item_content TEXT NOT NULL, -- JSON item_text TEXT NOT NULL, -- textual representation created_at TEXT NOT NULL DEFAULT (datetime('now')), diff --git a/src/Simplex/Chat/Migrations/M20221001_shared_msg_id_indices.hs b/src/Simplex/Chat/Migrations/M20221001_shared_msg_id_indices.hs new file mode 100644 index 0000000000..10ac0fa5e1 --- /dev/null +++ b/src/Simplex/Chat/Migrations/M20221001_shared_msg_id_indices.hs @@ -0,0 +1,25 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Migrations.M20221001_shared_msg_id_indices where + +import Database.SQLite.Simple (Query) +import Database.SQLite.Simple.QQ (sql) + +m20221001_shared_msg_id_indices :: Query +m20221001_shared_msg_id_indices = + [sql| +DROP INDEX idx_messages_group_shared_msg_id; + +CREATE UNIQUE INDEX idx_chat_items_direct_shared_msg_id ON chat_items( + user_id, + contact_id, + shared_msg_id +); + +CREATE UNIQUE INDEX idx_chat_items_group_shared_msg_id ON chat_items( + user_id, + group_id, + group_member_id, + shared_msg_id +); +|] diff --git a/src/Simplex/Chat/Migrations/chat_schema.sql b/src/Simplex/Chat/Migrations/chat_schema.sql index 2924e1acbd..427c10a223 100644 --- a/src/Simplex/Chat/Migrations/chat_schema.sql +++ b/src/Simplex/Chat/Migrations/chat_schema.sql @@ -331,8 +331,8 @@ CREATE TABLE chat_items( created_by_msg_id INTEGER UNIQUE REFERENCES messages(message_id) ON DELETE SET NULL, item_sent INTEGER NOT NULL, -- 0 for received, 1 for sent item_ts TEXT NOT NULL, -- broker_ts of creating message for received, created_at for sent - item_deleted INTEGER NOT NULL DEFAULT 0, -- 1 for deleted, - item_content TEXT NOT NULL, -- JSON + item_deleted INTEGER NOT NULL DEFAULT 0, -- 1 for deleted, -- ! legacy field that was used for group chat items when they weren't fully deleted +item_content TEXT NOT NULL, -- JSON item_text TEXT NOT NULL, -- textual representation created_at TEXT NOT NULL DEFAULT(datetime('now')), updated_at TEXT NOT NULL DEFAULT(datetime('now')) @@ -374,11 +374,6 @@ CREATE UNIQUE INDEX idx_messages_direct_shared_msg_id ON messages( shared_msg_id_user, shared_msg_id ); -CREATE UNIQUE INDEX idx_messages_group_shared_msg_id ON messages( - group_id, - shared_msg_id_user, - shared_msg_id -); CREATE INDEX idx_chat_items_shared_msg_id ON chat_items(shared_msg_id); CREATE TABLE calls( -- stores call invitations state for communicating state between NSE and app when call notification comes @@ -420,3 +415,14 @@ CREATE TABLE settings( created_at TEXT NOT NULL DEFAULT(datetime('now')), updated_at TEXT NOT NULL DEFAULT(datetime('now')) ); +CREATE UNIQUE INDEX idx_chat_items_direct_shared_msg_id ON chat_items( + user_id, + contact_id, + shared_msg_id +); +CREATE UNIQUE INDEX idx_chat_items_group_shared_msg_id ON chat_items( + user_id, + group_id, + group_member_id, + shared_msg_id +); diff --git a/src/Simplex/Chat/Store.hs b/src/Simplex/Chat/Store.hs index f52175af22..4ad006321c 100644 --- a/src/Simplex/Chat/Store.hs +++ b/src/Simplex/Chat/Store.hs @@ -171,9 +171,8 @@ module Simplex.Chat.Store deleteDirectChatItemLocal, deleteDirectChatItemRcvBroadcast, updateGroupChatItem, - deleteGroupChatItemInternal, + deleteGroupChatItemLocal, deleteGroupChatItemRcvBroadcast, - deleteGroupChatItemSndBroadcast, updateDirectChatItemsRead, updateGroupChatItemsRead, getSMPServers, @@ -256,13 +255,13 @@ import Simplex.Chat.Migrations.M20220824_profiles_local_alias import Simplex.Chat.Migrations.M20220909_commands import Simplex.Chat.Migrations.M20220926_connection_alias import Simplex.Chat.Migrations.M20220928_settings +import Simplex.Chat.Migrations.M20221001_shared_msg_id_indices import Simplex.Chat.Protocol import Simplex.Chat.Types import Simplex.Messaging.Agent.Protocol (ACorrId, AgentMsgId, ConnId, InvitationId, MsgMeta (..)) import Simplex.Messaging.Agent.Store.SQLite (SQLiteStore (..), createSQLiteStore, firstRow, firstRow', maybeFirstRow, withTransaction) import Simplex.Messaging.Agent.Store.SQLite.Migrations (Migration (..)) import qualified Simplex.Messaging.Crypto as C -import Simplex.Messaging.Encoding.String (StrEncoding (strEncode)) import Simplex.Messaging.Parsers (dropPrefix, sumTypeJSON) import Simplex.Messaging.Protocol (ProtocolServer (..), SMPServer, pattern SMPServer) import Simplex.Messaging.Transport.Client (TransportHost) @@ -293,7 +292,8 @@ schemaMigrations = ("20220824_profiles_local_alias", m20220824_profiles_local_alias), ("20220909_commands", m20220909_commands), ("20220926_connection_alias", m20220926_connection_alias), - ("20220928_settings", m20220928_settings) + ("20220928_settings", m20220928_settings), + ("20221001_shared_msg_id_indices", m20221001_shared_msg_id_indices) ] -- | The list of migrations in ascending order by date @@ -3422,23 +3422,6 @@ deleteChatItemMessages_ db itemId = |] (Only itemId) -setChatItemMessagesDeleted_ :: DB.Connection -> ChatItemId -> IO () -setChatItemMessagesDeleted_ db itemId = - DB.execute - db - [sql| - UPDATE messages - SET chat_msg_event = ?, msg_body = ? - WHERE message_id IN ( - SELECT message_id - FROM chat_item_messages - WHERE chat_item_id = ? - ) - |] - (XMsgDeleted_, xMsgDeletedBody, itemId) - where - xMsgDeletedBody = strEncode ChatMessage {msgId = Nothing, chatMsgEvent = XMsgDeleted} - deleteDirectChatItemRcvBroadcast :: DB.Connection -> UserId -> Contact -> ChatItemId -> MessageId -> ExceptT StoreError IO AChatItem deleteDirectChatItemRcvBroadcast db userId ct itemId msgId = do currentTs <- liftIO getCurrentTime @@ -3461,17 +3444,6 @@ updateDirectChatItemRcvDeleted_ db userId ct@Contact {contactId} itemId currentT (toContent, toText, currentTs, userId, contactId, itemId) pure $ AChatItem SCTDirect msgDir (DirectChat ct) (ci {content = toContent, meta = (meta ci) {itemText = toText}, formattedText = Nothing}) -deleteQuote_ :: DB.Connection -> ChatItemId -> IO () -deleteQuote_ db itemId = - DB.execute - db - [sql| - UPDATE chat_items - SET quoted_shared_msg_id = NULL, quoted_sent_at = NULL, quoted_content = NULL, quoted_sent = NULL, quoted_member_id = NULL - WHERE chat_item_id = ? - |] - (Only itemId) - getDirectChatItemBySharedMsgId :: DB.Connection -> UserId -> ContactId -> SharedMsgId -> ExceptT StoreError IO (CChatItem 'CTDirect) getDirectChatItemBySharedMsgId db userId contactId sharedMsgId = do itemId <- getDirectChatItemIdBySharedMsgId_ db userId contactId sharedMsgId @@ -3554,51 +3526,49 @@ updateGroupChatItem db user@User {userId} groupId itemId newContent msgId = do correctDir :: CChatItem c -> Either StoreError (ChatItem c d) correctDir (CChatItem _ ci) = first SEInternalError $ checkDirection ci -deleteGroupChatItemInternal :: DB.Connection -> User -> GroupInfo -> ChatItemId -> ExceptT StoreError IO AChatItem -deleteGroupChatItemInternal db user gInfo itemId = do - currentTs <- liftIO getCurrentTime - ci <- deleteGroupChatItem_ db user gInfo itemId CIDMInternal True currentTs - liftIO $ setChatItemMessagesDeleted_ db itemId - liftIO $ DB.execute db "DELETE FROM files WHERE chat_item_id = ?" (Only itemId) - pure ci +deleteGroupChatItemLocal :: DB.Connection -> User -> GroupInfo -> ChatItemId -> CIDeleteMode -> ExceptT StoreError IO AChatItem +deleteGroupChatItemLocal db user gInfo itemId mode = do + liftIO $ deleteChatItemMessages_ db itemId + deleteGroupChatItem_ db user gInfo itemId mode -deleteGroupChatItemRcvBroadcast :: DB.Connection -> User -> GroupInfo -> ChatItemId -> MessageId -> ExceptT StoreError IO AChatItem -deleteGroupChatItemRcvBroadcast db user gInfo itemId = - deleteGroupChatItemBroadcast_ db user gInfo itemId False - -deleteGroupChatItemSndBroadcast :: DB.Connection -> User -> GroupInfo -> ChatItemId -> MessageId -> ExceptT StoreError IO AChatItem -deleteGroupChatItemSndBroadcast db user gInfo itemId msgId = do - ci <- deleteGroupChatItemBroadcast_ db user gInfo itemId True msgId - liftIO $ setChatItemMessagesDeleted_ db itemId - liftIO $ DB.execute db "DELETE FROM files WHERE chat_item_id = ?" (Only itemId) - pure ci - -deleteGroupChatItemBroadcast_ :: DB.Connection -> User -> GroupInfo -> ChatItemId -> Bool -> MessageId -> ExceptT StoreError IO AChatItem -deleteGroupChatItemBroadcast_ db user gInfo itemId itemDeleted msgId = do - currentTs <- liftIO getCurrentTime - liftIO $ insertChatItemMessage_ db itemId msgId currentTs - deleteGroupChatItem_ db user gInfo itemId CIDMBroadcast itemDeleted currentTs - -deleteGroupChatItem_ :: DB.Connection -> User -> GroupInfo -> ChatItemId -> CIDeleteMode -> Bool -> UTCTime -> ExceptT StoreError IO AChatItem -deleteGroupChatItem_ db user@User {userId} gInfo@GroupInfo {groupId} itemId mode itemDeleted currentTs = do +deleteGroupChatItem_ :: DB.Connection -> User -> GroupInfo -> ChatItemId -> CIDeleteMode -> ExceptT StoreError IO AChatItem +deleteGroupChatItem_ db user@User {userId} gInfo@GroupInfo {groupId} itemId mode = do (CChatItem msgDir ci) <- getGroupChatItem db user groupId itemId let toContent = msgDirToDeletedContent_ msgDir mode liftIO $ do DB.execute db [sql| - UPDATE chat_items - SET item_content = ?, item_text = ?, item_deleted = ?, updated_at = ? + DELETE FROM chat_items WHERE user_id = ? AND group_id = ? AND chat_item_id = ? |] - (toContent, toText, itemDeleted, currentTs, userId, groupId, itemId) - when itemDeleted $ deleteQuote_ db itemId - pure $ AChatItem SCTGroup msgDir (GroupChat gInfo) (ci {content = toContent, meta = (meta ci) {itemText = toText, itemDeleted}, formattedText = Nothing}) - where - toText = ciDeleteModeToText mode + (userId, groupId, itemId) + pure $ AChatItem SCTGroup msgDir (GroupChat gInfo) (ci {content = toContent, meta = (meta ci) {itemText = ciDeleteModeToText mode, itemDeleted = True}, formattedText = Nothing}) -getGroupChatItemBySharedMsgId :: DB.Connection -> User -> Int64 -> SharedMsgId -> ExceptT StoreError IO (CChatItem 'CTGroup) -getGroupChatItemBySharedMsgId db user@User {userId} groupId sharedMsgId = do +deleteGroupChatItemRcvBroadcast :: DB.Connection -> User -> GroupInfo -> ChatItemId -> MessageId -> ExceptT StoreError IO AChatItem +deleteGroupChatItemRcvBroadcast db user gInfo itemId msgId = do + currentTs <- liftIO getCurrentTime + liftIO $ insertChatItemMessage_ db itemId msgId currentTs + updateGroupChatItemRcvDeleted_ db user gInfo itemId currentTs + +updateGroupChatItemRcvDeleted_ :: DB.Connection -> User -> GroupInfo -> ChatItemId -> UTCTime -> ExceptT StoreError IO AChatItem +updateGroupChatItemRcvDeleted_ db user@User {userId} gInfo@GroupInfo {groupId} itemId currentTs = do + (CChatItem msgDir ci) <- getGroupChatItem db user groupId itemId + let toContent = msgDirToDeletedContent_ msgDir CIDMBroadcast + toText = ciDeleteModeToText CIDMBroadcast + liftIO $ do + DB.execute + db + [sql| + UPDATE chat_items + SET item_content = ?, item_text = ?, updated_at = ? + WHERE user_id = ? AND group_id = ? AND chat_item_id = ? + |] + (toContent, toText, currentTs, userId, groupId, itemId) + pure $ AChatItem SCTGroup msgDir (GroupChat gInfo) (ci {content = toContent, meta = (meta ci) {itemText = toText}, formattedText = Nothing}) + +getGroupChatItemBySharedMsgId :: DB.Connection -> User -> GroupId -> GroupMemberId -> SharedMsgId -> ExceptT StoreError IO (CChatItem 'CTGroup) +getGroupChatItemBySharedMsgId db user@User {userId} groupId groupMemberId sharedMsgId = do itemId <- ExceptT . firstRow fromOnly (SEChatItemSharedMsgIdNotFound sharedMsgId) $ DB.query @@ -3606,11 +3576,11 @@ getGroupChatItemBySharedMsgId db user@User {userId} groupId sharedMsgId = do [sql| SELECT chat_item_id FROM chat_items - WHERE user_id = ? AND group_id = ? AND shared_msg_id = ? + WHERE user_id = ? AND group_id = ? AND group_member_id = ? AND shared_msg_id = ? ORDER BY chat_item_id DESC LIMIT 1 |] - (userId, groupId, sharedMsgId) + (userId, groupId, groupMemberId, sharedMsgId) getGroupChatItem db user groupId itemId getGroupChatItem :: DB.Connection -> User -> Int64 -> ChatItemId -> ExceptT StoreError IO (CChatItem 'CTGroup) @@ -4109,7 +4079,7 @@ getChatsWithExpiredItems db User {userId} expirationDate = [sql| SELECT contact_id, group_id FROM chat_items - WHERE user_id = ? AND item_ts <= ? AND item_deleted != 1 + WHERE user_id = ? AND item_ts <= ? GROUP BY contact_id, group_id ORDER BY contact_id ASC, group_id ASC |] @@ -4143,7 +4113,7 @@ getGroupExpiredCIs db User {userId} groupId expirationDate = SELECT i.chat_item_id, f.file_id, f.ci_file_status, f.file_path FROM chat_items i LEFT JOIN files f ON f.chat_item_id = i.chat_item_id - WHERE i.user_id = ? AND i.group_id = ? AND i.item_ts <= ? AND i.item_deleted != 1 + WHERE i.user_id = ? AND i.group_id = ? AND i.item_ts <= ? ORDER BY i.item_ts ASC |] (userId, groupId, expirationDate) diff --git a/tests/ChatTests.hs b/tests/ChatTests.hs index e3532130bb..5fab927576 100644 --- a/tests/ChatTests.hs +++ b/tests/ChatTests.hs @@ -1008,17 +1008,15 @@ testGroupMessageDelete = (bob <# "#team alice> hello!") (cath <# "#team alice> hello!") + -- alice: deletes msg id 5 alice #$> ("/_delete item #1 5 internal", id, "message deleted") alice #$> ("/_get chat #1 count=1", chat, [(0, "connected")]) bob #$> ("/_get chat #1 count=1", chat, [(0, "hello!")]) cath #$> ("/_get chat #1 count=1", chat, [(0, "hello!")]) - alice #$> ("/_update item #1 5 text updating deleted message", id, "cannot update this item") - alice #$> ("/_send #1 json {\"quotedItemId\": 5, \"msgContent\": {\"type\": \"text\", \"text\": \"quoting deleted message\"}}", id, "cannot reply to this message") - threadDelay 1000000 - -- alice, bob: msg id 6, cath: msg id 5 + -- alice: msg id 5, bob: msg id 6, cath: msg id 5 bob `send` "> #team @alice (hello) hi alic" bob <# "#team > alice hello!" bob <## " hi alic" @@ -1036,17 +1034,14 @@ testGroupMessageDelete = bob #$> ("/_get chat #1 count=2", chat', [((0, "hello!"), Nothing), ((1, "hi alic"), Just (0, "hello!"))]) cath #$> ("/_get chat #1 count=2", chat', [((0, "hello!"), Nothing), ((0, "hi alic"), Just (0, "hello!"))]) - alice #$> ("/_delete item #1 5 broadcast", id, "message deleted") - concurrently_ - (bob <# "#team alice> [deleted] hello!") - (cath <# "#team alice> [deleted] hello!") - - alice #$> ("/_delete item #1 6 internal", id, "message deleted") + -- alice: deletes msg id 5 + alice #$> ("/_delete item #1 5 internal", id, "message deleted") alice #$> ("/_get chat #1 count=1", chat', [((0, "connected"), Nothing)]) - bob #$> ("/_get chat #1 count=2", chat', [((0, "this item is deleted (broadcast)"), Nothing), ((1, "hi alic"), Just (0, "hello!"))]) - cath #$> ("/_get chat #1 count=2", chat', [((0, "this item is deleted (broadcast)"), Nothing), ((0, "hi alic"), Just (0, "hello!"))]) + bob #$> ("/_get chat #1 count=2", chat', [((0, "hello!"), Nothing), ((1, "hi alic"), Just (0, "hello!"))]) + cath #$> ("/_get chat #1 count=2", chat', [((0, "hello!"), Nothing), ((0, "hi alic"), Just (0, "hello!"))]) + -- alice: msg id 5 bob #$> ("/_update item #1 6 text hi alice", id, "message updated") concurrently_ (alice <# "#team bob> [edited] hi alice") @@ -1056,11 +1051,11 @@ testGroupMessageDelete = ) alice #$> ("/_get chat #1 count=1", chat', [((0, "hi alice"), Nothing)]) - bob #$> ("/_get chat #1 count=2", chat', [((0, "this item is deleted (broadcast)"), Nothing), ((1, "hi alice"), Just (0, "hello!"))]) - cath #$> ("/_get chat #1 count=2", chat', [((0, "this item is deleted (broadcast)"), Nothing), ((0, "hi alice"), Just (0, "hello!"))]) + bob #$> ("/_get chat #1 count=2", chat', [((0, "hello!"), Nothing), ((1, "hi alice"), Just (0, "hello!"))]) + cath #$> ("/_get chat #1 count=2", chat', [((0, "hello!"), Nothing), ((0, "hi alice"), Just (0, "hello!"))]) threadDelay 1000000 - -- alice, bob: msg id 7, cath: msg id 6 + -- alice: msg id 6, bob: msg id 7, cath: msg id 6 cath #> "#team how are you?" concurrently_ (alice <# "#team cath> how are you?") @@ -1071,12 +1066,12 @@ testGroupMessageDelete = (alice <# "#team cath> [deleted] how are you?") (bob <# "#team cath> [deleted] how are you?") - alice #$> ("/_delete item #1 6 broadcast", id, "cannot delete this item") - alice #$> ("/_delete item #1 6 internal", id, "message deleted") + alice #$> ("/_delete item #1 5 broadcast", id, "cannot delete this item") + alice #$> ("/_delete item #1 5 internal", id, "message deleted") alice #$> ("/_get chat #1 count=1", chat', [((0, "this item is deleted (broadcast)"), Nothing)]) - bob #$> ("/_get chat #1 count=3", chat', [((0, "this item is deleted (broadcast)"), Nothing), ((1, "hi alice"), Just (0, "hello!")), ((0, "this item is deleted (broadcast)"), Nothing)]) - cath #$> ("/_get chat #1 count=2", chat', [((0, "this item is deleted (broadcast)"), Nothing), ((0, "hi alice"), Just (0, "hello!"))]) + bob #$> ("/_get chat #1 count=3", chat', [((0, "hello!"), Nothing), ((1, "hi alice"), Just (0, "hello!")), ((0, "this item is deleted (broadcast)"), Nothing)]) + cath #$> ("/_get chat #1 count=2", chat', [((0, "hello!"), Nothing), ((0, "hi alice"), Just (0, "hello!"))]) testUpdateGroupProfile :: IO () testUpdateGroupProfile = @@ -2413,27 +2408,44 @@ testGetSetSMPServers = testAsyncInitiatingOffline :: IO () testAsyncInitiatingOffline = withTmpFiles $ do + putStrLn "testAsyncInitiatingOffline" inv <- withNewTestChat "alice" aliceProfile $ \alice -> do + putStrLn "1" alice ##> "/c" + putStrLn "2" getInvitation alice + putStrLn "3" withNewTestChat "bob" bobProfile $ \bob -> do + putStrLn "4" bob `send` ("/c " <> inv) + putStrLn "5" bob <### ["/c " <> inv, "confirmation sent!"] + putStrLn "6" withTestChat "alice" $ \alice -> do + putStrLn "7" concurrently_ (bob <## "alice (Alice): contact is connected") (alice <## "bob (Bob): contact is connected") testAsyncAcceptingOffline :: IO () testAsyncAcceptingOffline = withTmpFiles $ do + putStrLn "testAsyncAcceptingOffline" inv <- withNewTestChat "alice" aliceProfile $ \alice -> do + putStrLn "1" alice ##> "/c" + putStrLn "2" getInvitation alice + putStrLn "3" withNewTestChat "bob" bobProfile $ \bob -> do + putStrLn "4" bob ##> ("/c " <> inv) + putStrLn "5" bob <## "confirmation sent!" - withTestChat "alice" $ \alice -> - withTestChat "bob" $ \bob -> + putStrLn "6" + withTestChat "alice" $ \alice -> do + putStrLn "7" + withTestChat "bob" $ \bob -> do + putStrLn "8" concurrently_ (bob <## "alice (Alice): contact is connected") (alice <## "bob (Bob): contact is connected")