-
Notifications
You must be signed in to change notification settings - Fork 13k
fix: clean up lastMessage field when deleting user to prevent ghost m… #37360
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
fix: clean up lastMessage field when deleting user to prevent ghost m… #37360
Conversation
…essages (RocketChat#36885) - Add cleanup logic in deleteUser function to update lastMessage field - Find rooms where deleted user had the last message - Update lastMessage to previous valid message or unset if none exists - Add comprehensive test coverage for the fix Fixes: RocketChat#36885
|
|
Looks like this PR is not ready to merge, because of the following issues:
Please fix the issues and try again If you have any trouble, please check the PR guidelines |
WalkthroughAdds post-deletion cleanup to reset room.lastMessage when a deleted user's message was stored as the room's lastMessage. If Message_ErasureType is Delete and Store_Last_Message is enabled, affected rooms are scanned and lastMessage is replaced with the most recent visible message. Changes
Sequence Diagram(s)sequenceDiagram
participant Client
participant DeleteUser as DeleteUserHandler
participant RoomDB as RoomDB
participant MessageDB as MessageDB
Client->>DeleteUser: Request delete user
DeleteUser->>DeleteUser: Check Message_ErasureType & Store_Last_Message
alt Erase=Delete and Store_Last_Message=true
DeleteUser->>RoomDB: Find rooms where lastMessage.user == deletedUser
RoomDB-->>DeleteUser: Matching rooms list
loop For each matching room
DeleteUser->>MessageDB: Query last visible message for room
MessageDB-->>DeleteUser: Last visible message (or none)
DeleteUser->>RoomDB: Update room.lastMessage (set to message or null)
end
end
DeleteUser-->>Client: Confirm user deleted and lastMessage cleaned
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes
Possibly related PRs
Suggested labels
Suggested reviewers
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
Tip 📝 Customizable high-level summaries are now available in beta!You can now customize how CodeRabbit generates the high-level summary in your pull requests — including its content, structure, tone, and formatting.
Example instruction:
Note: This feature is currently in beta for Pro-tier users, and pricing will be announced later. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
🧹 Nitpick comments (4)
apps/meteor/app/lib/server/functions/deleteUser.ts (2)
100-103: Consider adding error handling for the cleanup operations.If
Messages.getLastVisibleUserMessageSentByRoomIdorRooms.resetLastMessageByIdfail, the entire user deletion could fail. Consider wrapping the loop in a try-catch or handling errors per-room to prevent blocking the deletion process.for (const room of roomsWithDeletedUserLastMessage) { + try { const lastMessageNotDeleted = await Messages.getLastVisibleUserMessageSentByRoomId(room._id); await Rooms.resetLastMessageById(room._id, lastMessageNotDeleted); + } catch (error) { + // Log error but continue with deletion + console.error(`Failed to reset lastMessage for room ${room._id}:`, error); + } }
100-103: Consider performance implications of sequential processing.The synchronous loop processes rooms one at a time. For users who authored the last message in many rooms, this could slow down the deletion process. Consider using
Promise.allor batch processing.-for (const room of roomsWithDeletedUserLastMessage) { - const lastMessageNotDeleted = await Messages.getLastVisibleUserMessageSentByRoomId(room._id); - await Rooms.resetLastMessageById(room._id, lastMessageNotDeleted); -} +await Promise.all( + roomsWithDeletedUserLastMessage.map(async (room) => { + const lastMessageNotDeleted = await Messages.getLastVisibleUserMessageSentByRoomId(room._id); + await Rooms.resetLastMessageById(room._id, lastMessageNotDeleted); + }) +);apps/meteor/tests/end-to-end/api/users.ts (2)
3386-3420: Clarify expected behavior in test assertions.The test doesn't clearly assert the expected state after deletion. Since the test only sends one message (from the target user), after deletion there should be no messages left in the room. The assertion should explicitly verify that
lastMessageis eitherundefinedornull, not just check if it exists.Apply this diff to make the expected behavior explicit:
// Verify lastMessage is cleaned up const roomAfterDelete = await request .get(api('channels.info')) .set(credentials) .query({ roomId: room._id }) .expect(200); -// lastMessage should either be undefined or point to a different valid message -if (roomAfterDelete.body.channel.lastMessage) { - expect(roomAfterDelete.body.channel.lastMessage.u).to.not.have.property('_id', targetUser._id); -} +// Since the target user's message was the only message, lastMessage should be undefined after cleanup +expect(roomAfterDelete.body.channel.lastMessage).to.be.undefined;
3422-3468: Refactor test setup for clarity.The test setup is confusing because
beforeEachsends a message fromtargetUserthat becomes irrelevant in this test scenario. The test then sends an admin message followed by anothertargetUsermessage. This creates three messages total, making the flow harder to understand.Consider moving the admin message to
beforeEachand removing the redundant first target user message from the test, or better yet, create a separatebeforeEachfor this specific test.-it('should update lastMessage to previous message when deleting user whose message was the last', async () => { +it('should update lastMessage to previous message when deleting user whose message was the last', async () => { await updatePermission('delete-user', ['admin']); - // Send another message as admin user BEFORE the target user's message + // Clear the existing message and set up a clean test scenario + // Send admin message first await request .post(api('chat.sendMessage')) .set(credentials) .send({ message: { rid: room._id, - msg: 'Admin message before target user message', + msg: 'Admin message', }, }) .expect(200); // Send the target user message (will be lastMessage) const targetUserCredentials = await login(targetUser.username, password); await request .post(api('chat.sendMessage')) .set(targetUserCredentials) .send({ message: { rid: room._id, - msg: 'Target user last message', + msg: 'Target user message', }, }) .expect(200);Alternatively, consider using a separate describe block with its own
beforeEachthat doesn't send the initial target user message.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
Disabled knowledge base sources:
- Jira integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (2)
apps/meteor/app/lib/server/functions/deleteUser.ts(1 hunks)apps/meteor/tests/end-to-end/api/users.ts(1 hunks)
🧰 Additional context used
🧠 Learnings (12)
📚 Learning: 2025-09-16T22:08:51.490Z
Learnt from: CR
Repo: RocketChat/Rocket.Chat PR: 0
File: .cursor/rules/playwright.mdc:0-0
Timestamp: 2025-09-16T22:08:51.490Z
Learning: Applies to apps/meteor/tests/e2e/**/*.spec.ts : Use descriptive test names that clearly communicate expected behavior
Applied to files:
apps/meteor/tests/end-to-end/api/users.ts
📚 Learning: 2025-09-16T22:08:51.490Z
Learnt from: CR
Repo: RocketChat/Rocket.Chat PR: 0
File: .cursor/rules/playwright.mdc:0-0
Timestamp: 2025-09-16T22:08:51.490Z
Learning: Applies to apps/meteor/tests/e2e/**/*.spec.ts : Ensure a clean state for each test execution
Applied to files:
apps/meteor/tests/end-to-end/api/users.ts
📚 Learning: 2025-09-16T22:08:51.490Z
Learnt from: CR
Repo: RocketChat/Rocket.Chat PR: 0
File: .cursor/rules/playwright.mdc:0-0
Timestamp: 2025-09-16T22:08:51.490Z
Learning: Applies to apps/meteor/tests/e2e/**/*.spec.ts : Ensure tests run reliably in parallel without shared state conflicts
Applied to files:
apps/meteor/tests/end-to-end/api/users.ts
📚 Learning: 2025-09-16T22:08:51.490Z
Learnt from: CR
Repo: RocketChat/Rocket.Chat PR: 0
File: .cursor/rules/playwright.mdc:0-0
Timestamp: 2025-09-16T22:08:51.490Z
Learning: Applies to apps/meteor/tests/e2e/**/*.spec.ts : Maintain test isolation between test cases
Applied to files:
apps/meteor/tests/end-to-end/api/users.ts
📚 Learning: 2025-09-16T22:08:51.490Z
Learnt from: CR
Repo: RocketChat/Rocket.Chat PR: 0
File: .cursor/rules/playwright.mdc:0-0
Timestamp: 2025-09-16T22:08:51.490Z
Learning: Applies to apps/meteor/tests/e2e/**/*.spec.ts : Use test.beforeAll() and test.afterAll() for setup and teardown
Applied to files:
apps/meteor/tests/end-to-end/api/users.ts
📚 Learning: 2025-09-16T22:08:51.490Z
Learnt from: CR
Repo: RocketChat/Rocket.Chat PR: 0
File: .cursor/rules/playwright.mdc:0-0
Timestamp: 2025-09-16T22:08:51.490Z
Learning: Applies to apps/meteor/tests/e2e/**/*.spec.ts : Group related tests in the same file
Applied to files:
apps/meteor/tests/end-to-end/api/users.ts
📚 Learning: 2025-09-16T22:08:51.490Z
Learnt from: CR
Repo: RocketChat/Rocket.Chat PR: 0
File: .cursor/rules/playwright.mdc:0-0
Timestamp: 2025-09-16T22:08:51.490Z
Learning: Applies to apps/meteor/tests/e2e/**/*.spec.ts : Utilize Playwright fixtures (test, page, expect) consistently
Applied to files:
apps/meteor/tests/end-to-end/api/users.ts
📚 Learning: 2025-09-16T22:08:51.490Z
Learnt from: CR
Repo: RocketChat/Rocket.Chat PR: 0
File: .cursor/rules/playwright.mdc:0-0
Timestamp: 2025-09-16T22:08:51.490Z
Learning: Applies to apps/meteor/tests/e2e/**/*.{ts,tsx,js,jsx} : Avoid code comments in the implementation
Applied to files:
apps/meteor/tests/end-to-end/api/users.ts
📚 Learning: 2025-09-16T22:08:51.490Z
Learnt from: CR
Repo: RocketChat/Rocket.Chat PR: 0
File: .cursor/rules/playwright.mdc:0-0
Timestamp: 2025-09-16T22:08:51.490Z
Learning: Applies to apps/meteor/tests/e2e/**/*.spec.ts : Use test.step() to organize complex test scenarios
Applied to files:
apps/meteor/tests/end-to-end/api/users.ts
📚 Learning: 2025-09-16T22:08:51.490Z
Learnt from: CR
Repo: RocketChat/Rocket.Chat PR: 0
File: .cursor/rules/playwright.mdc:0-0
Timestamp: 2025-09-16T22:08:51.490Z
Learning: Applies to apps/meteor/tests/e2e/**/*.{ts,tsx,js,jsx} : Write concise, technical TypeScript/JavaScript with accurate typing
Applied to files:
apps/meteor/tests/end-to-end/api/users.ts
📚 Learning: 2025-09-16T13:33:49.237Z
Learnt from: cardoso
Repo: RocketChat/Rocket.Chat PR: 36890
File: apps/meteor/tests/e2e/e2e-encryption/e2ee-otr.spec.ts:21-26
Timestamp: 2025-09-16T13:33:49.237Z
Learning: In Rocket.Chat test files, the im.delete API endpoint accepts either a `roomId` parameter (requiring the actual DM room _id) or a `username` parameter (for the DM partner's username). It does not accept slug-like constructions such as concatenating usernames together.
Applied to files:
apps/meteor/app/lib/server/functions/deleteUser.ts
📚 Learning: 2025-09-16T13:33:49.237Z
Learnt from: cardoso
Repo: RocketChat/Rocket.Chat PR: 36890
File: apps/meteor/tests/e2e/e2e-encryption/e2ee-otr.spec.ts:21-26
Timestamp: 2025-09-16T13:33:49.237Z
Learning: The im.delete API endpoint accepts either a `roomId` parameter (requiring the actual DM room _id) or a `username` parameter (for the DM partner's username). Constructing slug-like identifiers like `user2${Users.userE2EE.data.username}` doesn't work for this endpoint.
Applied to files:
apps/meteor/app/lib/server/functions/deleteUser.ts
🧬 Code graph analysis (1)
apps/meteor/tests/end-to-end/api/users.ts (4)
apps/meteor/tests/data/users.helper.ts (2)
TestUser(8-8)createUser(10-36)apps/meteor/tests/data/api-data.ts (2)
request(10-10)credentials(39-42)apps/meteor/tests/e2e/utils/create-target-channel.ts (1)
deleteRoom(48-50)apps/meteor/app/lib/server/functions/deleteUser.ts (1)
deleteUser(34-193)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
🧹 Nitpick comments (1)
apps/meteor/app/lib/server/functions/deleteUser.ts (1)
93-105: Consider using cursor iteration for better memory efficiency.The cleanup logic is well-placed and correctly handles the ghost message issue. However, using
.toArray()on line 99 loads all matching rooms into memory, which could be problematic if a user's message is the last message in many rooms.Consider iterating with a cursor for better scalability:
- const roomsWithDeletedUserLastMessage = await Rooms.find({ - 'lastMessage.u._id': userId, - }).toArray(); - - for (const room of roomsWithDeletedUserLastMessage) { + const roomsCursor = Rooms.find({ + 'lastMessage.u._id': userId, + }); + + for await (const room of roomsCursor) { const lastMessageNotDeleted = await Messages.getLastVisibleUserMessageSentByRoomId(room._id); await Rooms.resetLastMessageById(room._id, lastMessageNotDeleted); }This pattern is already used elsewhere in the same file (lines 76-81) for file deletion.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
Disabled knowledge base sources:
- Jira integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (1)
apps/meteor/app/lib/server/functions/deleteUser.ts(1 hunks)
🧰 Additional context used
🧠 Learnings (4)
📚 Learning: 2025-11-19T18:20:37.116Z
Learnt from: gabriellsh
Repo: RocketChat/Rocket.Chat PR: 37419
File: apps/meteor/server/services/media-call/service.ts:141-141
Timestamp: 2025-11-19T18:20:37.116Z
Learning: In apps/meteor/server/services/media-call/service.ts, the sendHistoryMessage method should use call.caller.id or call.createdBy?.id as the message author, not call.transferredBy?.id. Even for transferred calls, the message should appear in the DM between the two users who are calling each other, not sent by the person who transferred the call.
Applied to files:
apps/meteor/app/lib/server/functions/deleteUser.ts
📚 Learning: 2025-09-25T09:59:26.461Z
Learnt from: Dnouv
Repo: RocketChat/Rocket.Chat PR: 37057
File: packages/apps-engine/src/definition/accessors/IUserRead.ts:23-27
Timestamp: 2025-09-25T09:59:26.461Z
Learning: UserBridge.doGetUserRoomIds in packages/apps-engine/src/server/bridges/UserBridge.ts has a bug where it implicitly returns undefined when the app lacks read permission (missing return statement in the else case of the permission check).
Applied to files:
apps/meteor/app/lib/server/functions/deleteUser.ts
📚 Learning: 2025-09-16T13:33:49.237Z
Learnt from: cardoso
Repo: RocketChat/Rocket.Chat PR: 36890
File: apps/meteor/tests/e2e/e2e-encryption/e2ee-otr.spec.ts:21-26
Timestamp: 2025-09-16T13:33:49.237Z
Learning: In Rocket.Chat test files, the im.delete API endpoint accepts either a `roomId` parameter (requiring the actual DM room _id) or a `username` parameter (for the DM partner's username). It does not accept slug-like constructions such as concatenating usernames together.
Applied to files:
apps/meteor/app/lib/server/functions/deleteUser.ts
📚 Learning: 2025-09-16T13:33:49.237Z
Learnt from: cardoso
Repo: RocketChat/Rocket.Chat PR: 36890
File: apps/meteor/tests/e2e/e2e-encryption/e2ee-otr.spec.ts:21-26
Timestamp: 2025-09-16T13:33:49.237Z
Learning: The im.delete API endpoint accepts either a `roomId` parameter (requiring the actual DM room _id) or a `username` parameter (for the DM partner's username). Constructing slug-like identifiers like `user2${Users.userE2EE.data.username}` doesn't work for this endpoint.
Applied to files:
apps/meteor/app/lib/server/functions/deleteUser.ts
🔇 Additional comments (1)
apps/meteor/app/lib/server/functions/deleteUser.ts (1)
96-96: Good defensive check with the Store_Last_Message setting.The conditional check ensures cleanup only runs when the
Store_Last_Messagesetting is enabled, which is correct since thelastMessagefield would only be populated when this setting is active. This prevents unnecessary database queries when the feature is disabled.Additionally, placing this cleanup within the
'Delete'case (and not in'Unlink'or'Keep') is the right choice, as those cases handle messages differently and wouldn't create ghost messages.
Proposed changes (including videos or screenshots)
Problem
When a user is deleted from Rocket.Chat, their messages are properly removed from the
rocketchat_messagecollection, but thelastMessagefield inrocketchat_roomdocuments is not cleaned up. This field contains embedded user data that becomes stale after user deletion, causing "ghost messages" to appear across all clients.Root Cause
The
lastMessagefield structure: