Skip to content
5 changes: 5 additions & 0 deletions .changeset/fast-phones-poke.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@rocket.chat/meteor': patch
---

Fixes `channels.messages`, `groups.messages`, `dm.messages` and `im.messages` APIs to filter out deleted messages.
1 change: 1 addition & 0 deletions apps/meteor/app/api/server/v1/channels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,7 @@ API.v1.addRoute(
...parseIds(mentionIds, 'mentions._id'),
...parseIds(starredIds, 'starred._id'),
...(pinned && pinned.toLowerCase() === 'true' ? { pinned: true } : {}),
_hidden: { $ne: true },
};

if (!(await canAccessRoomAsync(findResult, { _id: this.userId }))) {
Expand Down
1 change: 1 addition & 0 deletions apps/meteor/app/api/server/v1/groups.ts
Original file line number Diff line number Diff line change
Expand Up @@ -792,6 +792,7 @@ API.v1.addRoute(
...parseIds(mentionIds, 'mentions._id'),
...parseIds(starredIds, 'starred._id'),
...(pinned && pinned.toLowerCase() === 'true' ? { pinned: true } : {}),
_hidden: { $ne: true },
};

const { cursor, totalCount } = Messages.findPaginated(ourQuery, {
Expand Down
1 change: 1 addition & 0 deletions apps/meteor/app/api/server/v1/im.ts
Original file line number Diff line number Diff line change
Expand Up @@ -455,6 +455,7 @@ API.v1.addRoute(
...parseIds(mentionIds, 'mentions._id'),
...parseIds(starredIds, 'starred._id'),
...(pinned && pinned.toLowerCase() === 'true' ? { pinned: true } : {}),
_hidden: { $ne: true },
};
const sortObj = sort || { ts: -1 };

Expand Down
29 changes: 27 additions & 2 deletions apps/meteor/tests/data/chat.helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,17 @@ export const starMessage = ({ messageId, requestCredentials }: { messageId: IMes
.send({ messageId });
};

export const pinMessage = ({ messageId, requestCredentials }: { messageId: IMessage['_id']; requestCredentials?: Credentials }) => {
export const pinMessage = ({
messageId,
requestCredentials,
unpin = false,
}: {
messageId: IMessage['_id'];
requestCredentials?: Credentials;
unpin?: boolean;
}) => {
return request
.post(api('chat.pinMessage'))
.post(api(unpin ? 'chat.unPinMessage' : 'chat.pinMessage'))
.set(requestCredentials ?? credentials)
.send({ messageId });
};
Expand Down Expand Up @@ -96,3 +104,20 @@ export const followMessage = ({ msgId, requestCredentials }: { msgId: IMessage['
.set(requestCredentials ?? credentials)
.send({ mid: msgId });
};

export const updateMessage = ({
msgId,
requestCredentials,
updatedMessage,
roomId,
}: {
msgId: IMessage['_id'];
requestCredentials?: Credentials;
updatedMessage: string;
roomId?: IRoom['_id'];
}) => {
return request
.post(api('chat.update'))
.set(requestCredentials ?? credentials)
.send({ msgId, text: updatedMessage, roomId });
};
77 changes: 76 additions & 1 deletion apps/meteor/tests/end-to-end/api/channels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { expect, assert } from 'chai';
import { after, before, describe, it } from 'mocha';

import { getCredentials, api, request, credentials, reservedWords } from '../../data/api-data';
import { pinMessage, sendMessage, starMessage } from '../../data/chat.helper';
import { pinMessage, sendMessage, starMessage, updateMessage } from '../../data/chat.helper';
import { CI_MAX_ROOMS_PER_GUEST as maxRoomsPerGuest } from '../../data/constants';
import { createIntegration, removeIntegration } from '../../data/integration.helper';
import { updatePermission, updateSetting } from '../../data/permissions.helper';
Expand Down Expand Up @@ -3946,6 +3946,7 @@ describe('[Channels]', () => {
let emptyChannel: IRoom;
let firstUser: IUser;
let secondUser: IUser;
let pinnedMessageId: IMessage['_id'];

before(async () => {
await updatePermission('view-c-room', ['admin', 'user', 'bot', 'app', 'anonymous']);
Expand Down Expand Up @@ -3982,6 +3983,7 @@ describe('[Channels]', () => {
starMessage({ messageId: starredMessage.body.message._id }),
pinMessage({ messageId: pinnedMessage.body.message._id }),
]);
pinnedMessageId = pinnedMessage.body.message._id;
});

after(async () => {
Expand Down Expand Up @@ -4125,6 +4127,79 @@ describe('[Channels]', () => {
});
});

describe('_hidden messages behavior when Message_KeepHistory is enabled', async () => {
before(async () => {
await updateSetting('Message_KeepHistory', true);
await pinMessage({ messageId: pinnedMessageId, unpin: true });
});

after(async () => {
await updateSetting('Message_KeepHistory', false);
});

it('should return all messages, without any pinned messages', async () => {
await request
.get(api('channels.messages'))
.set(credentials)
.query({ roomId: testChannel._id })
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('messages').and.to.be.an('array');
expect(res.body.messages).to.have.lengthOf(5);

res.body.messages.forEach((msg: IMessage) => {
expect(msg).to.not.have.property('pinned', true);
expect(msg).to.not.have.property('_hidden');
});
});
});

it('should return no pinned messages', async () => {
await request
.get(api('channels.messages'))
.set(credentials)
.query({
roomId: testChannel._id,
pinned: true,
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
expect(res.body.messages).to.have.lengthOf(0);
expect(res.body).to.have.property('count', 0);
expect(res.body).to.have.property('total', 0);
});
});

it('should not return old message when updating a message', async () => {
await updateMessage({ msgId: pinnedMessageId, updatedMessage: 'message was unpinned', roomId: testChannel._id });

await request
.get(api('channels.messages'))
.set(credentials)
.query({ roomId: testChannel._id })
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('messages').and.to.be.an('array');
expect(res.body.messages).to.have.lengthOf(5);

const updatedMessage = res.body.messages.find((msg: IMessage) => msg._id === pinnedMessageId);

expect(updatedMessage).to.have.property('msg', 'message was unpinned');
expect(updatedMessage).to.have.property('editedAt');

res.body.messages.forEach((msg: IMessage) => {
expect(msg).to.not.have.property('_hidden');
});
});
});
});

describe('Additional Visibility Tests', () => {
let outsiderUser: IUser;
let insideUser: IUser;
Expand Down
80 changes: 79 additions & 1 deletion apps/meteor/tests/end-to-end/api/direct-message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { expect } from 'chai';
import { after, before, describe, it } from 'mocha';

import { getCredentials, api, request, credentials, apiUsername, apiEmail, methodCall } from '../../data/api-data';
import { pinMessage, sendMessage, starMessage } from '../../data/chat.helper';
import { pinMessage, sendMessage, starMessage, updateMessage } from '../../data/chat.helper';
import { updateSetting, updatePermission } from '../../data/permissions.helper';
import { deleteRoom } from '../../data/rooms.helper';
import { testFileUploads } from '../../data/uploads.helper';
Expand Down Expand Up @@ -482,6 +482,7 @@ describe('[Direct Messages]', () => {
let testUserDMRoom: IRoom;
let testUserCredentials: Credentials;
let testUser2Credentials: Credentials;
let pinnedMessageId: IMessage['_id'];

let messages: Pick<IMessage, 'rid' | 'msg' | 'mentions'>[] = [];

Expand Down Expand Up @@ -538,6 +539,7 @@ describe('[Direct Messages]', () => {
starMessage({ messageId: starredMessage.body.message._id, requestCredentials: testUserCredentials }),
pinMessage({ messageId: pinnedMessage.body.message._id, requestCredentials: testUserCredentials }),
]);
pinnedMessageId = pinnedMessage.body.message._id;
});

after(async () => Promise.all([deleteUser(testUser), deleteUser(testUser2)]));
Expand Down Expand Up @@ -702,6 +704,82 @@ describe('[Direct Messages]', () => {
expect(res.body).to.have.property('total', 1);
});
});

describe('_hidden messages behavior when Message_KeepHistory is enabled', async () => {
before(async () => {
await updateSetting('Message_KeepHistory', true);
await pinMessage({ messageId: pinnedMessageId, unpin: true, requestCredentials: testUserCredentials });
});

after(async () => {
await updateSetting('Message_KeepHistory', false);
});

it('should return all messages, without any pinned messages', async () => {
await request
.get(api('im.messages'))
.set(testUserCredentials)
.query({ roomId: testUserDMRoom._id })
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('messages').and.to.be.an('array');
expect(res.body.messages).to.have.lengthOf(5);

res.body.messages.forEach((msg: IMessage) => {
expect(msg).to.not.have.property('pinned', true);
expect(msg).to.not.have.property('_hidden');
});
});
});

it('should return no pinned messages', async () => {
await request
.get(api('im.messages'))
.set(testUserCredentials)
.query({
roomId: testUserDMRoom._id,
pinned: true,
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
expect(res.body.messages).to.have.lengthOf(0);
expect(res.body).to.have.property('count', 0);
expect(res.body).to.have.property('total', 0);
});
});

it('should not return old message when updating a message', async () => {
await updateMessage({
msgId: pinnedMessageId,
updatedMessage: 'message was unpinned',
roomId: testUserDMRoom._id,
requestCredentials: testUser2Credentials,
});

await request
.get(api('im.messages'))
.set(testUserCredentials)
.query({ roomId: testUserDMRoom._id })
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('messages').and.to.be.an('array');
expect(res.body.messages).to.have.lengthOf(5);
const updatedMessage = res.body.messages.find((msg: IMessage) => msg._id === pinnedMessageId);
expect(updatedMessage).to.have.property('msg', 'message was unpinned');
expect(updatedMessage).to.have.property('editedAt');

res.body.messages.forEach((msg: IMessage) => {
expect(msg).to.not.have.property('_hidden');
});
});
});
});
});

describe('/im.messages.others', () => {
Expand Down
78 changes: 77 additions & 1 deletion apps/meteor/tests/end-to-end/api/groups.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { assert, expect } from 'chai';
import { after, before, describe, it } from 'mocha';

import { getCredentials, api, request, credentials, apiPrivateChannelName } from '../../data/api-data';
import { pinMessage, starMessage, sendMessage } from '../../data/chat.helper';
import { pinMessage, starMessage, sendMessage, updateMessage } from '../../data/chat.helper';
import { CI_MAX_ROOMS_PER_GUEST as maxRoomsPerGuest } from '../../data/constants';
import { createGroup, deleteGroup } from '../../data/groups.helper';
import { createIntegration, removeIntegration } from '../../data/integration.helper';
Expand Down Expand Up @@ -500,6 +500,7 @@ describe('[Groups]', () => {
let testGroup: IRoom;
let firstUser: IUser;
let secondUser: IUser;
let pinnedMessageId: IMessage['_id'];

before(async () => {
testGroup = (await createGroup({ name: `test-group-${Date.now()}` })).body.group;
Expand Down Expand Up @@ -533,6 +534,8 @@ describe('[Groups]', () => {
starMessage({ messageId: starredMessage.body.message._id }),
pinMessage({ messageId: pinnedMessage.body.message._id }),
]);

pinnedMessageId = pinnedMessage.body.message._id;
});

after(async () => {
Expand Down Expand Up @@ -670,6 +673,79 @@ describe('[Groups]', () => {
await Promise.all([deleteGroup({ roomName: secondGroup.name }), deleteGroup({ roomName: thirdGroup.name })]);
}
});

describe('_hidden messages behavior when Message_KeepHistory is enabled', async () => {
before(async () => {
await updateSetting('Message_KeepHistory', true);
await pinMessage({ messageId: pinnedMessageId, unpin: true });
});

after(async () => {
await updateSetting('Message_KeepHistory', false);
});

it('should return all messages, without any pinned messages', async () => {
await request
.get(api('groups.messages'))
.set(credentials)
.query({ roomId: testGroup._id })
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('messages').and.to.be.an('array');
expect(res.body.messages).to.have.lengthOf(5);

res.body.messages.forEach((msg: IMessage) => {
expect(msg).to.not.have.property('pinned', true);
expect(msg).to.not.have.property('_hidden');
});
});
});

it('should return no pinned messages', async () => {
await request
.get(api('groups.messages'))
.set(credentials)
.query({
roomId: testGroup._id,
pinned: true,
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
expect(res.body.messages).to.have.lengthOf(0);
expect(res.body).to.have.property('count', 0);
expect(res.body).to.have.property('total', 0);
});
});

it('should not return old message when updating a message', async () => {
await updateMessage({ msgId: pinnedMessageId, updatedMessage: 'message was unpinned', roomId: testGroup._id });

await request
.get(api('groups.messages'))
.set(credentials)
.query({ roomId: testGroup._id })
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('messages').and.to.be.an('array');
expect(res.body.messages).to.have.lengthOf(5);

const updatedMessage = res.body.messages.find((msg: IMessage) => msg._id === pinnedMessageId);

expect(updatedMessage).to.have.property('msg', 'message was unpinned');
expect(updatedMessage).to.have.property('editedAt');

res.body.messages.forEach((msg: IMessage) => {
expect(msg).to.not.have.property('_hidden');
});
});
});
});
});

describe('/groups.invite', async () => {
Expand Down
Loading