diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index 6b8115d7c..d98bea82f 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -537,6 +537,8 @@ processChatCommand = \case maxItemTs_ <- withStore' $ \db -> getGroupMaxItemTs db user gInfo forM_ filesInfo $ \fileInfo -> deleteFile user fileInfo withStore' $ \db -> deleteGroupCIs db user gInfo + membersToDelete <- withStore' $ \db -> getGroupMembersForExpiration db user gInfo + forM_ membersToDelete $ \m -> withStore' $ \db -> deleteGroupMember db user m gInfo' <- case maxItemTs_ of Just ts -> do withStore' $ \db -> updateGroupTs db user gInfo ts @@ -905,7 +907,10 @@ processChatCommand = \case ci <- saveSndChatItem user (CDGroupSnd gInfo) msg (CISndGroupEvent $ SGEMemberDeleted memberId (fromLocalProfile memberProfile)) Nothing Nothing toView . CRNewChatItem $ AChatItem SCTGroup SMDSnd (GroupChat gInfo) ci deleteMemberConnection user m - withStore' $ \db -> updateGroupMemberStatus db userId m GSMemRemoved + withStore' $ \db -> + checkGroupMemberHasItems db user m >>= \case + Just _ -> updateGroupMemberStatus db userId m GSMemRemoved + Nothing -> deleteGroupMember db user m pure $ CRUserDeletedMember gInfo m {memberStatus = GSMemRemoved} APILeaveGroup groupId -> withUser $ \user@User {userId} -> do Group gInfo@GroupInfo {membership} members <- withStore $ \db -> getGroup db user groupId @@ -1536,6 +1541,8 @@ expireChatItems user ttl sync = do maxItemTs_ <- withStore' $ \db -> getGroupMaxItemTs db user gInfo forM_ filesInfo $ \fileInfo -> deleteFile user fileInfo withStore' $ \db -> deleteGroupExpiredCIs db user gInfo expirationDate createdAtCutoff + membersToDelete <- withStore' $ \db -> getGroupMembersForExpiration db user gInfo + forM_ membersToDelete $ \m -> withStore' $ \db -> deleteGroupMember db user m withStore' $ \db -> do ciCount_ <- getGroupCICount db user gInfo case (maxItemTs_, ciCount_) of diff --git a/src/Simplex/Chat/Store.hs b/src/Simplex/Chat/Store.hs index a36c20543..1e25b37cd 100644 --- a/src/Simplex/Chat/Store.hs +++ b/src/Simplex/Chat/Store.hs @@ -83,6 +83,7 @@ module Simplex.Chat.Store getGroupInfoByName, getGroupMember, getGroupMembers, + getGroupMembersForExpiration, deleteGroupConnectionsAndFiles, deleteGroupItemsAndMembers, deleteGroup, @@ -98,6 +99,7 @@ module Simplex.Chat.Store updateGroupMemberStatus, updateGroupMemberStatusById, createNewGroupMember, + checkGroupMemberHasItems, deleteGroupMember, deleteGroupMemberConnection, updateGroupMemberRole, @@ -1676,9 +1678,7 @@ deleteGroupItemsAndMembers :: DB.Connection -> User -> GroupInfo -> [GroupMember deleteGroupItemsAndMembers db user@User {userId} GroupInfo {groupId} members = do DB.execute db "DELETE FROM chat_items WHERE user_id = ? AND group_id = ?" (userId, groupId) DB.execute db "DELETE FROM group_members WHERE user_id = ? AND group_id = ?" (userId, groupId) - forM_ members $ \m@GroupMember {groupMemberId, memberContactId, memberContactProfileId} -> unless (isJust memberContactId) $ do - sameProfileMember :: (Maybe GroupMemberId) <- maybeFirstRow fromOnly $ DB.query db "SELECT group_member_id FROM group_members WHERE user_id = ? AND contact_profile_id = ? AND group_member_id != ? LIMIT 1" (userId, memberContactProfileId, groupMemberId) - unless (isJust sameProfileMember) $ deleteMemberProfileAndName_ db user m + forM_ members $ \m -> cleanupMemberContactAndProfile_ db user m deleteGroup :: DB.Connection -> User -> GroupInfo -> IO () deleteGroup db User {userId} GroupInfo {groupId, localDisplayName} = do @@ -1779,6 +1779,32 @@ getGroupMembers db user@User {userId, userContactId} GroupInfo {groupId} = do |] (groupId, userId, userContactId) +getGroupMembersForExpiration :: DB.Connection -> User -> GroupInfo -> IO [GroupMember] +getGroupMembersForExpiration db user@User {userId, userContactId} GroupInfo {groupId} = do + map (toContactMember user) + <$> DB.query + db + [sql| + SELECT + m.group_member_id, m.group_id, m.member_id, m.member_role, m.member_category, m.member_status, + m.invited_by, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.image, p.local_alias, + c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.custom_user_profile_id, + c.conn_status, c.conn_type, 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 + FROM group_members m + JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id) + LEFT JOIN connections c ON c.connection_id = ( + SELECT max(cc.connection_id) + FROM connections cc + where cc.group_member_id = m.group_member_id + ) + WHERE m.group_id = ? AND m.user_id = ? AND (m.contact_id IS NULL OR m.contact_id != ?) + AND m.member_status IN (?, ?, ?) + AND m.group_member_id NOT IN ( + SELECT DISTINCT group_member_id FROM chat_items + ) + |] + (groupId, userId, userContactId, GSMemRemoved, GSMemLeft, GSMemGroupDeleted) + toContactMember :: User -> (GroupMemberRow :. MaybeConnectionRow) -> GroupMember toContactMember User {userContactId} (memberRow :. connRow) = (toGroupMember userContactId memberRow) {activeConn = toMaybeConnection connRow} @@ -1986,19 +2012,30 @@ createNewMember_ groupMemberId <- insertedRowId db pure GroupMember {groupMemberId, groupId, memberId, memberRole, memberCategory, memberStatus, invitedBy, localDisplayName, memberProfile = toLocalProfile memberContactProfileId memberProfile "", memberContactId, memberContactProfileId, activeConn} +checkGroupMemberHasItems :: DB.Connection -> User -> GroupMember -> IO (Maybe ChatItemId) +checkGroupMemberHasItems db User {userId} GroupMember {groupMemberId, groupId} = + maybeFirstRow fromOnly $ DB.query db "SELECT chat_item_id FROM chat_items WHERE user_id = ? AND group_id = ? AND group_member_id = ? LIMIT 1" (userId, groupId, groupMemberId) + deleteGroupMember :: DB.Connection -> User -> GroupMember -> IO () -deleteGroupMember db user@User {userId} m@GroupMember {groupMemberId, groupId, memberContactId, memberContactProfileId} = do +deleteGroupMember db user@User {userId} m@GroupMember {groupMemberId, groupId} = do deleteGroupMemberConnection db user m DB.execute db "DELETE FROM chat_items WHERE user_id = ? AND group_id = ? AND group_member_id = ?" (userId, groupId, groupMemberId) DB.execute db "DELETE FROM group_members WHERE user_id = ? AND group_member_id = ?" (userId, groupMemberId) - unless (isJust memberContactId) $ do - sameProfileMember :: (Maybe GroupMemberId) <- maybeFirstRow fromOnly $ DB.query db "SELECT group_member_id FROM group_members WHERE user_id = ? AND contact_profile_id = ? AND group_member_id != ? LIMIT 1" (userId, memberContactProfileId, groupMemberId) - unless (isJust sameProfileMember) $ deleteMemberProfileAndName_ db user m + cleanupMemberContactAndProfile_ db user m -deleteMemberProfileAndName_ :: DB.Connection -> User -> GroupMember -> IO () -deleteMemberProfileAndName_ db User {userId} GroupMember {memberContactProfileId, localDisplayName} = do - DB.execute db "DELETE FROM contact_profiles WHERE user_id = ? AND contact_profile_id = ?" (userId, memberContactProfileId) - DB.execute db "DELETE FROM display_names WHERE user_id = ? AND local_display_name = ?" (userId, localDisplayName) +cleanupMemberContactAndProfile_ :: DB.Connection -> User -> GroupMember -> IO () +cleanupMemberContactAndProfile_ db User {userId} GroupMember {groupMemberId, localDisplayName, memberContactId, memberContactProfileId} = + case memberContactId of + Just contactId -> + runExceptT (getContact db userId contactId) >>= \case + Right ct@Contact {activeConn = Connection {connLevel, viaGroupLink}, contactUsed} -> + unless ((connLevel == 0 && not viaGroupLink) || contactUsed) $ deleteContact db userId ct + _ -> pure () + Nothing -> do + sameProfileMember :: (Maybe GroupMemberId) <- maybeFirstRow fromOnly $ DB.query db "SELECT group_member_id FROM group_members WHERE user_id = ? AND contact_profile_id = ? AND group_member_id != ? LIMIT 1" (userId, memberContactProfileId, groupMemberId) + unless (isJust sameProfileMember) $ do + DB.execute db "DELETE FROM contact_profiles WHERE user_id = ? AND contact_profile_id = ?" (userId, memberContactProfileId) + DB.execute db "DELETE FROM display_names WHERE user_id = ? AND local_display_name = ?" (userId, localDisplayName) deleteGroupMemberConnection :: DB.Connection -> User -> GroupMember -> IO () deleteGroupMemberConnection db User {userId} GroupMember {groupMemberId} = @@ -3122,7 +3159,7 @@ getDirectChatPreviews_ db User {userId} = do ) ChatStats ON ChatStats.contact_id = ct.contact_id LEFT JOIN chat_items ri ON i.quoted_shared_msg_id = ri.shared_msg_id WHERE ct.user_id = ? - AND ((c.conn_level = 0 AND c.via_group_link = 0) OR i.chat_item_id IS NOT NULL OR ct.contact_used = 1) + AND ((c.conn_level = 0 AND c.via_group_link = 0) OR ct.contact_used = 1) AND c.connection_id = ( SELECT cc_connection_id FROM ( SELECT diff --git a/tests/ChatTests.hs b/tests/ChatTests.hs index 410637b95..77521b8a1 100644 --- a/tests/ChatTests.hs +++ b/tests/ChatTests.hs @@ -768,7 +768,13 @@ testGroupDelete = cath <## "#team: you deleted the group" alice <##> bob alice <##> cath - bob <##> cath + -- unused group contacts are deleted + bob ##> "@cath hi" + bob <## "no contact cath" + (cath "@bob hi" + cath <## "no contact bob" + (bob ("/_get chat #1 count=100", chat, []) alice @@@ [("#team", "")] - alice <##> bob - alice @@@ [("@bob", "hey"), ("#team", "")] + -- removing member deletes unused group contact + alice ##> "@bob hi" + alice <## "no contact bob" + (bob "/j team" bob <## "error: connection authorization failed - this could happen if connection was deleted, secured with different credentials, or due to a bug - please re-create the connection" -- repeat request is prohibited because of the re-used XContactId, until contact is deleted @@ -3568,11 +3576,11 @@ testGroupLinkDeleteInvitedMemberNoBrokenItem = bob <## "alice: contact is deleted" bob ##> ("/c " <> gLink) bob <## "connection request sent!" - alice <## "bob_1 (Bob): accepting request to join group #team..." + alice <## "bob (Bob): accepting request to join group #team..." concurrentlyN_ [ do - alice <## "bob_1 (Bob): contact is connected" - alice <## "bob_1 invited to group #team via your group link", + alice <## "bob (Bob): contact is connected" + alice <## "bob invited to group #team via your group link", do bob <## "alice_1 (Alice): contact is connected" bob <## "#team_1 (team): alice_1 invites you to join the group as member" @@ -3580,12 +3588,12 @@ testGroupLinkDeleteInvitedMemberNoBrokenItem = ] bob ##> "/j team_1" concurrently_ - (alice <## "#team: bob_1 joined the group") + (alice <## "#team: bob joined the group") (bob <## "#team_1: you joined the group") alice #> "#team hello" bob <# "#team_1 alice_1> hello" bob #> "#team_1 hi there" - alice <# "#team bob_1> hi there" + alice <# "#team bob> hi there" withTestChatContactConnected :: String -> (TestCC -> IO a) -> IO a withTestChatContactConnected dbPrefix action =