From 69bd1b7db0ee79dc47cc094964972ff0fa6803b8 Mon Sep 17 00:00:00 2001 From: Ricardo Garim Date: Sat, 26 Oct 2024 13:26:29 -0300 Subject: [PATCH 1/2] chore: remove query field on messages listing --- apps/meteor/app/api/server/v1/channels.ts | 20 ++- apps/meteor/tests/data/chat.helper.ts | 13 ++ apps/meteor/tests/end-to-end/api/channels.ts | 142 +++++++++++++++++- .../src/v1/channels/ChannelsMessagesProps.ts | 22 ++- 4 files changed, 188 insertions(+), 9 deletions(-) diff --git a/apps/meteor/app/api/server/v1/channels.ts b/apps/meteor/app/api/server/v1/channels.ts index 09c4bee3da694..91c7b63c2098b 100644 --- a/apps/meteor/app/api/server/v1/channels.ts +++ b/apps/meteor/app/api/server/v1/channels.ts @@ -279,15 +279,25 @@ API.v1.addRoute( }, { async get() { - const { roomId } = this.queryParams; + const { roomId, mentionIds, starredIds, pinned } = this.queryParams; + const { offset, count } = await getPaginationItems(this.queryParams); + const { sort, fields, query } = await this.parseJsonQuery(); + const findResult = await findChannelByIdOrName({ params: { roomId }, checkedArchived: false, }); - const { offset, count } = await getPaginationItems(this.queryParams); - const { sort, fields, query } = await this.parseJsonQuery(); - const ourQuery = { ...query, rid: findResult._id }; + const parseIds = (ids: string | undefined, field: string) => + typeof ids === 'string' && ids ? { [field]: { $in: ids.split(',').map((id) => id.trim()) } } : {}; + + const ourQuery = { + ...query, + rid: findResult._id, + ...parseIds(mentionIds, 'mentions._id'), + ...parseIds(starredIds, 'starred._id'), + ...(pinned && pinned.toLowerCase() === 'true' ? { pinned: true } : {}), + }; // Special check for the permissions if ( @@ -297,7 +307,7 @@ API.v1.addRoute( return API.v1.unauthorized(); } - const { cursor, totalCount } = await Messages.findPaginated(ourQuery, { + const { cursor, totalCount } = Messages.findPaginated(ourQuery, { sort: sort || { ts: -1 }, skip: offset, limit: count, diff --git a/apps/meteor/tests/data/chat.helper.ts b/apps/meteor/tests/data/chat.helper.ts index 46514969bd821..4c34dffd7016c 100644 --- a/apps/meteor/tests/data/chat.helper.ts +++ b/apps/meteor/tests/data/chat.helper.ts @@ -22,6 +22,7 @@ export const sendSimpleMessage = ({ rid: roomId, text, }; + if (tmid) { message.tmid = tmid; } @@ -29,6 +30,18 @@ export const sendSimpleMessage = ({ return request.post(api('chat.sendMessage')).set(credentials).send({ message }); }; +export const sendMessage = ({ message }: { message: { rid: IRoom['_id']; msg: string } & Partial> }) => { + return request.post(api('chat.sendMessage')).set(credentials).send({ message }); +}; + +export const starMessage = ({ messageId }: { messageId: IMessage['_id'] }) => { + return request.post(api('chat.starMessage')).set(credentials).send({ messageId }); +}; + +export const pinMessage = ({ messageId }: { messageId: IMessage['_id'] }) => { + return request.post(api('chat.pinMessage')).set(credentials).send({ messageId }); +}; + export const deleteMessage = ({ roomId, msgId }: { roomId: IRoom['_id']; msgId: IMessage['_id'] }) => { if (!roomId) { throw new Error('"roomId" is required in "deleteMessage" test helper'); diff --git a/apps/meteor/tests/end-to-end/api/channels.ts b/apps/meteor/tests/end-to-end/api/channels.ts index 61a1c294afde9..75aff5ad770af 100644 --- a/apps/meteor/tests/end-to-end/api/channels.ts +++ b/apps/meteor/tests/end-to-end/api/channels.ts @@ -4,6 +4,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 { CI_MAX_ROOMS_PER_GUEST as maxRoomsPerGuest } from '../../data/constants'; import { createIntegration, removeIntegration } from '../../data/integration.helper'; import { updatePermission, updateSetting } from '../../data/permissions.helper'; @@ -2461,9 +2462,45 @@ describe('[Channels]', () => { describe('[/channels.messages]', () => { let testChannel: IRoom; + let emptyChannel: IRoom; + let firstUser: IUser; + let secondUser: IUser; + before(async () => { await updatePermission('view-c-room', ['admin', 'user', 'bot', 'app', 'anonymous']); + emptyChannel = (await createRoom({ type: 'c', name: `channels.messages.empty.test.${Date.now()}` })).body.channel; testChannel = (await createRoom({ type: 'c', name: `channels.messages.test.${Date.now()}` })).body.channel; + + firstUser = await createUser({ joinDefaultChannels: false }); + secondUser = await createUser({ joinDefaultChannels: false }); + + const messages = [ + { + rid: testChannel._id, + msg: `@${firstUser.username} youre being mentioned`, + mentions: [{ username: firstUser.username, _id: firstUser._id, name: firstUser.name }], + }, + { + rid: testChannel._id, + msg: `@${secondUser.username} youre being mentioned`, + mentions: [{ username: secondUser.username, _id: secondUser._id, name: secondUser.name }], + }, + { + rid: testChannel._id, + msg: `A simple message`, + }, + { + rid: testChannel._id, + msg: `A pinned simple message`, + }, + ]; + + const [, , starredMessage, pinnedMessage] = await Promise.all(messages.map((message) => sendMessage({ message }))); + + await Promise.all([ + starMessage({ messageId: starredMessage.body.message._id }), + pinMessage({ messageId: pinnedMessage.body.message._id }), + ]); }); after(async () => { @@ -2476,7 +2513,7 @@ describe('[Channels]', () => { .get(api('channels.messages')) .set(credentials) .query({ - roomId: testChannel._id, + roomId: emptyChannel._id, }) .expect('Content-Type', 'application/json') .expect(200) @@ -2488,6 +2525,26 @@ describe('[Channels]', () => { }); }); + it('should return an array of messages when inspecting a room with 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').that.has.lengthOf(5); + expect(res.body).to.have.property('count', 5); + expect(res.body).to.have.property('total', 5); + + const pinnedMessage = res.body.messages.find((message: any) => message.t === 'message_pinned'); + expect(pinnedMessage).to.not.be.undefined; + }); + }); + it('should not return message when the user does NOT have the necessary permission', async () => { await updatePermission('view-c-room', []); await request @@ -2502,6 +2559,89 @@ describe('[Channels]', () => { expect(res.body).to.have.property('success', false); expect(res.body).to.have.property('error', 'User does not have the permissions required for this action [error-unauthorized]'); }); + await updatePermission('view-c-room', ['admin', 'user', 'bot', 'app', 'anonymous']); + }); + + it('should return messages that mention a single user', async () => { + await request + .get(api('channels.messages')) + .set(credentials) + .query({ + roomId: testChannel._id, + mentionIds: firstUser._id, + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body.messages).to.have.lengthOf(1); + expect(res.body.messages[0]).to.have.nested.property('mentions').that.is.an('array').and.to.have.lengthOf(1); + expect(res.body.messages[0].mentions[0]).to.have.property('_id', firstUser._id); + expect(res.body).to.have.property('count', 1); + expect(res.body).to.have.property('total', 1); + }); + }); + + it('should return messages that mention multiple users', async () => { + await request + .get(api('channels.messages')) + .set(credentials) + .query({ + roomId: testChannel._id, + mentionIds: `${firstUser._id},${secondUser._id}`, + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body.messages).to.have.lengthOf(2); + expect(res.body).to.have.property('count', 2); + expect(res.body).to.have.property('total', 2); + + const mentionIds = res.body.messages.map((message: any) => message.mentions[0]._id); + expect(mentionIds).to.include.members([firstUser._id, secondUser._id]); + }); + }); + + it('should return messages that are starred by a specific user', async () => { + await request + .get(api('channels.messages')) + .set(credentials) + .query({ + roomId: testChannel._id, + starredIds: 'rocketchat.internal.admin.test', + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body.messages).to.have.lengthOf(1); + expect(res.body.messages[0]).to.have.nested.property('starred').that.is.an('array').and.to.have.lengthOf(1); + expect(res.body).to.have.property('count', 1); + expect(res.body).to.have.property('total', 1); + }); + }); + + // Return messages that are pinned + it('should return messages that are pinned', 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(1); + expect(res.body.messages[0]).to.have.nested.property('pinned').that.is.an('boolean').and.to.be.true; + expect(res.body.messages[0]).to.have.nested.property('pinnedBy').that.is.an('object'); + expect(res.body.messages[0].pinnedBy).to.have.property('_id', 'rocketchat.internal.admin.test'); + expect(res.body).to.have.property('count', 1); + expect(res.body).to.have.property('total', 1); + }); }); }); }); diff --git a/packages/rest-typings/src/v1/channels/ChannelsMessagesProps.ts b/packages/rest-typings/src/v1/channels/ChannelsMessagesProps.ts index f09b7748babe9..dc78f2e77016e 100644 --- a/packages/rest-typings/src/v1/channels/ChannelsMessagesProps.ts +++ b/packages/rest-typings/src/v1/channels/ChannelsMessagesProps.ts @@ -5,8 +5,16 @@ import type { PaginatedRequest } from '../../helpers/PaginatedRequest'; const ajv = new Ajv({ coerceTypes: true }); -// query: { 'mentions._id': { $in: string[] } } | { 'starred._id': { $in: string[] } } | { pinned: boolean }; -export type ChannelsMessagesProps = PaginatedRequest<{ roomId: IRoom['_id'] }, 'ts'>; +export type ChannelsMessagesProps = PaginatedRequest< + { + roomId: IRoom['_id']; + mentionIds?: string; + starredIds?: string; + pinned?: boolean; + query?: Record; + }, + 'ts' +>; const channelsMessagesPropsSchema = { type: 'object', @@ -14,9 +22,17 @@ const channelsMessagesPropsSchema = { roomId: { type: 'string', }, + mentionIds: { + type: 'string', + }, + starredIds: { + type: 'string', + }, + pinned: { + type: 'string', + }, query: { type: 'string', - nullable: true, }, count: { type: 'number', From 09bb8bb34aa8fa74ea2a7acc19ca49dca435bba3 Mon Sep 17 00:00:00 2001 From: Ricardo Garim Date: Mon, 28 Oct 2024 08:19:57 -0300 Subject: [PATCH 2/2] docs: changeset --- .changeset/quiet-jokes-add.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/quiet-jokes-add.md diff --git a/.changeset/quiet-jokes-add.md b/.changeset/quiet-jokes-add.md new file mode 100644 index 0000000000000..6a6fb49971331 --- /dev/null +++ b/.changeset/quiet-jokes-add.md @@ -0,0 +1,6 @@ +--- +'@rocket.chat/rest-typings': major +'@rocket.chat/meteor': major +--- + +Changes channels messages listing endpoint by moving query params from the 'query' attribute to standard query parameters.