From 35d541a5a71fe13993e4f2d95b5687c83ec4eff0 Mon Sep 17 00:00:00 2001 From: Narasimha-sc <166327228+Narasimha-sc@users.noreply.github.com> Date: Mon, 7 Sep 2026 14:54:10 +0000 Subject: [PATCH] core: archive reports when removing member with messages Reports about a member's messages are authored by the reporter, so deleting the removed member's own chat items never reached them - unlike moderation, which archives them via markMessageReportsDeleted. Reports stayed in the reports list pointing at deleted messages. --- src/Simplex/Chat/Library/Commands.hs | 8 ++++--- src/Simplex/Chat/Library/Internal.hs | 8 +++++++ src/Simplex/Chat/Library/Subscriber.hs | 8 ++++--- src/Simplex/Chat/Store/Messages.hs | 15 +++++++++++++ tests/ChatTests/Groups.hs | 29 ++++++++++++++++++++++++++ 5 files changed, 62 insertions(+), 6 deletions(-) diff --git a/src/Simplex/Chat/Library/Commands.hs b/src/Simplex/Chat/Library/Commands.hs index 2f2a25d8fd..a12c7d0cd9 100644 --- a/src/Simplex/Chat/Library/Commands.hs +++ b/src/Simplex/Chat/Library/Commands.hs @@ -3171,9 +3171,11 @@ processChatCommand cxt nm = \case then void $ fullyDeleteMemberRecordIO db user gInfo m else void $ deleteOrUpdateMemberRecordIO db user gInfo m pure m {memberStatus = GSMemRemoved} - deleteMessages user gInfo@GroupInfo {membership} ms - | groupFeatureUserAllowed SGFFullDelete gInfo = deleteGroupMembersCIs user gInfo ms - | otherwise = markGroupMembersCIsDeleted user gInfo ms membership + deleteMessages user gInfo@GroupInfo {membership} ms = do + archiveMembersReports user gInfo ms membership True + if groupFeatureUserAllowed SGFFullDelete gInfo + then deleteGroupMembersCIs user gInfo ms + else markGroupMembersCIsDeleted user gInfo ms membership APILeaveGroup groupId -> withUser $ \user@User {userId} -> do gInfo@GroupInfo {membership} <- withFastStore $ \db -> getGroupInfo db cxt user groupId filesInfo <- withFastStore' $ \db -> getGroupFileInfo db user gInfo diff --git a/src/Simplex/Chat/Library/Internal.hs b/src/Simplex/Chat/Library/Internal.hs index 79b4578d8f..813e628b8a 100644 --- a/src/Simplex/Chat/Library/Internal.hs +++ b/src/Simplex/Chat/Library/Internal.hs @@ -647,6 +647,14 @@ markGroupMembersCIsDeleted user gInfo members byGroupMember = do filesInfo <- withStore' $ \db -> fmap concat $ forM members $ \m -> markGroupMemberCIsDeleted_ db user gInfo m byGroupMember deletedTs cancelFilesInProgress user filesInfo +-- Reports about a member's messages are authored by the reporter, so deleting the member's own +-- chat items never reaches them - they have to be archived separately, as moderation does. +archiveMembersReports :: User -> GroupInfo -> [GroupMember] -> GroupMember -> Bool -> CM () +archiveMembersReports user gInfo members byGroupMember byUser = do + deletedTs <- liftIO getCurrentTime + ciIds <- concat <$> withStore' (\db -> forM members $ \m -> markMemberReportsDeleted db user gInfo m byGroupMember deletedTs) + unless (null ciIds) $ toView $ CEvtGroupChatItemsDeleted user gInfo ciIds byUser (Just byGroupMember) + markGroupMemberCIsDeleted_ :: DB.Connection -> User -> GroupInfo -> GroupMember -> GroupMember -> UTCTime -> IO [CIFileInfo] markGroupMemberCIsDeleted_ db user gInfo member byGroupMember deletedTs = do fs <- getGroupMemberFileInfo db user gInfo member diff --git a/src/Simplex/Chat/Library/Subscriber.hs b/src/Simplex/Chat/Library/Subscriber.hs index 47625a9fd2..0981c72ec3 100644 --- a/src/Simplex/Chat/Library/Subscriber.hs +++ b/src/Simplex/Chat/Library/Subscriber.hs @@ -3654,9 +3654,11 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = (ci, cInfo) <- saveRcvChatItemNoParse user (CDGroupRcv gi' scopeInfo m') msg' brokerTs (CIRcvGroupEvent gEvent) groupMsgToView cInfo ci deleteMessages :: GroupInfo -> GroupMember -> CM () - deleteMessages gInfo' delMem - | groupFeatureMemberAllowed SGFFullDelete m gInfo' = deleteGroupMemberCIs user gInfo' delMem - | otherwise = markGroupMemberCIsDeleted user gInfo' delMem m + deleteMessages gInfo' delMem = do + archiveMembersReports user gInfo' [delMem] m False + if groupFeatureMemberAllowed SGFFullDelete m gInfo' + then deleteGroupMemberCIs user gInfo' delMem + else markGroupMemberCIsDeleted user gInfo' delMem m forwardToMember :: GroupMember -> CM () forwardToMember member = let fwd = GrpMsgForward {fwdSender = FwdMember (memberId' m) (memberShortenedName m), fwdBrokerTs = brokerTs} diff --git a/src/Simplex/Chat/Store/Messages.hs b/src/Simplex/Chat/Store/Messages.hs index 1cb90ab222..00a80a8610 100644 --- a/src/Simplex/Chat/Store/Messages.hs +++ b/src/Simplex/Chat/Store/Messages.hs @@ -73,6 +73,7 @@ module Simplex.Chat.Store.Messages markGroupChatItemBlocked, markGroupCIBlockedByAdmin, markMessageReportsDeleted, + markMemberReportsDeleted, markReceivedGroupReportsDeleted, deleteLocalChatItem, updateDirectChatItemsRead, @@ -3024,6 +3025,20 @@ markMessageReportsDeleted db User {userId} GroupInfo {groupId} ChatItem {meta = |] (DBCIDeleted, deletedTs, groupMemberId, currentTs, userId, groupId, MCReport_, itemSharedMsgId, DBCINotDeleted) +markMemberReportsDeleted :: DB.Connection -> User -> GroupInfo -> GroupMember -> GroupMember -> UTCTime -> IO [ChatItemId] +markMemberReportsDeleted db User {userId} GroupInfo {groupId} reportedMember byGroupMember deletedTs = do + currentTs <- liftIO getCurrentTime + map fromOnly + <$> DB.query + db + [sql| + UPDATE chat_items + SET item_deleted = ?, item_deleted_ts = ?, item_deleted_by_group_member_id = ?, updated_at = ? + WHERE user_id = ? AND group_id = ? AND msg_content_tag = ? AND quoted_member_id = ? AND item_deleted = ? + RETURNING chat_item_id; + |] + (DBCIDeleted, deletedTs, groupMemberId' byGroupMember, currentTs, userId, groupId, MCReport_, memberId' reportedMember, DBCINotDeleted) + markReceivedGroupReportsDeleted :: DB.Connection -> User -> GroupInfo -> UTCTime -> IO [ChatItemId] markReceivedGroupReportsDeleted db User {userId} GroupInfo {groupId, membership} deletedTs = do currentTs <- liftIO getCurrentTime diff --git a/tests/ChatTests/Groups.hs b/tests/ChatTests/Groups.hs index 2c01f379ad..4de3e4f294 100644 --- a/tests/ChatTests/Groups.hs +++ b/tests/ChatTests/Groups.hs @@ -219,6 +219,7 @@ chatGroupTests = do it "mark member inactive on reaching quota" testGroupMemberInactive describe "group member reports" $ do it "should send report to group owner, admins and moderators, but not other users" testGroupMemberReports + it "should archive reports about the messages of the member removed with messages" testGroupMemberReportsRemoveMember describe "group member mentions" $ do it "should send and edit messages with member mentions" testMemberMention it "should forward and quote message updating mentioned member name" testForwardQuoteMention @@ -7390,6 +7391,34 @@ testGroupMemberInactive ps = do } } +testGroupMemberReportsRemoveMember :: HasCallStack => TestParams -> IO () +testGroupMemberReportsRemoveMember = + testChat3 aliceProfile bobProfile cathProfile $ + \alice bob cath -> do + createGroup3' "jokes" alice (bob, GRMember) (cath, GRMember) + threadDelay 1000000 + bob #> "#jokes inappropriate joke" + concurrently_ + (alice <# "#jokes bob> inappropriate joke") + (cath <# "#jokes bob> inappropriate joke") + cath ##> "/report #jokes content inappropriate joke" + cath <# "#jokes (support) > bob inappropriate joke" + cath <## " report content" + alice <# "#jokes (support: cath) cath> > bob inappropriate joke" + alice <## " report content" + alice #$> ("/_get chat #1 content=report count=100", chat, [(0, "report content")]) + cath #$> ("/_get chat #1 content=report count=100", chat, [(1, "report content")]) + threadDelay 1000000 + alice ##> "/rm #jokes bob messages=on" + alice <## "#jokes: 1 messages deleted by user" + alice <## "#jokes: you removed bob from the group with all messages" + bob <## "#jokes: alice removed you from the group with all messages" + bob <## "use /d #jokes to delete the group" + cath <## "#jokes: 1 messages deleted by member alice" + cath <## "#jokes: alice removed bob from the group with all messages" + alice #$> ("/_get chat #1 content=report count=100", chat, [(0, "report content [marked deleted by you]")]) + cath #$> ("/_get chat #1 content=report count=100", chat, [(1, "report content [marked deleted by alice]")]) + testGroupMemberReports :: HasCallStack => TestParams -> IO () testGroupMemberReports = testChat4 aliceProfile bobProfile cathProfile danProfile $