diff --git a/apps/meteor/app/api/server/v1/groups.ts b/apps/meteor/app/api/server/v1/groups.ts index 64ad8e41150d..1a8069fff205 100644 --- a/apps/meteor/app/api/server/v1/groups.ts +++ b/apps/meteor/app/api/server/v1/groups.ts @@ -1,7 +1,7 @@ import { Team, isMeteorError } from '@rocket.chat/core-services'; import type { IIntegration, IUser, IRoom, RoomType } from '@rocket.chat/core-typings'; import { Integrations, Messages, Rooms, Subscriptions, Uploads, Users } from '@rocket.chat/models'; -import { isGroupsOnlineProps } from '@rocket.chat/rest-typings'; +import { isGroupsOnlineProps, isGroupsMessagesProps } from '@rocket.chat/rest-typings'; import { check, Match } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; import type { Filter } from 'mongodb'; @@ -746,19 +746,30 @@ API.v1.addRoute( API.v1.addRoute( 'groups.messages', - { authRequired: true }, + { authRequired: true, validateParams: isGroupsMessagesProps }, { async get() { + const { roomId, mentionIds, starredIds, pinned } = this.queryParams; + const findResult = await findPrivateGroupByIdOrName({ - params: this.queryParams, + params: { roomId }, userId: this.userId, }); const { offset, count } = await getPaginationItems(this.queryParams); const { sort, fields, query } = await this.parseJsonQuery(); - const ourQuery = Object.assign({}, query, { rid: findResult.rid }); + const parseIds = (ids: string | undefined, field: string) => + typeof ids === 'string' && ids ? { [field]: { $in: ids.split(',').map((id) => id.trim()) } } : {}; + + const ourQuery = { + ...query, + rid: findResult.rid, + ...parseIds(mentionIds, 'mentions._id'), + ...parseIds(starredIds, 'starred._id'), + ...(pinned && pinned.toLowerCase() === 'true' ? { pinned: true } : {}), + }; - 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 46514969bd82..2d531c644721 100644 --- a/apps/meteor/tests/data/chat.helper.ts +++ b/apps/meteor/tests/data/chat.helper.ts @@ -29,6 +29,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/groups.ts b/apps/meteor/tests/end-to-end/api/groups.ts index 9e7630bc5a0e..60a1590feb11 100644 --- a/apps/meteor/tests/end-to-end/api/groups.ts +++ b/apps/meteor/tests/end-to-end/api/groups.ts @@ -4,7 +4,9 @@ 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 { CI_MAX_ROOMS_PER_GUEST as maxRoomsPerGuest } from '../../data/constants'; +import { createGroup, deleteGroup } from '../../data/groups.helper'; import { createIntegration, removeIntegration } from '../../data/integration.helper'; import { updatePermission, updateSetting } from '../../data/permissions.helper'; import { createRoom } from '../../data/rooms.helper'; @@ -494,6 +496,145 @@ describe('[Groups]', () => { }); }); + describe('/groups.messages', () => { + let testGroup: IRoom; + let firstUser: IUser; + let secondUser: IUser; + + before(async () => { + testGroup = (await createGroup({ name: `test-group-${Date.now()}` })).body.group; + firstUser = await createUser({ joinDefaultChannels: false }); + secondUser = await createUser({ joinDefaultChannels: false }); + + const messages = [ + { + rid: testGroup._id, + msg: `@${firstUser.username} youre being mentioned`, + mentions: [{ username: firstUser.username, _id: firstUser._id, name: firstUser.name }], + }, + { + rid: testGroup._id, + msg: `@${secondUser.username} youre being mentioned`, + mentions: [{ username: secondUser.username, _id: secondUser._id, name: secondUser.name }], + }, + { + rid: testGroup._id, + msg: `A simple message`, + }, + { + rid: testGroup._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 () => { + await deleteGroup({ roomName: testGroup.name }); + }); + + it('should return all messages from a group', 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); + }); + }); + + it('should return messages that mention a single user', async () => { + await request + .get(api('groups.messages')) + .set(credentials) + .query({ + roomId: testGroup._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('groups.messages')) + .set(credentials) + .query({ + roomId: testGroup._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('groups.messages')) + .set(credentials) + .query({ + roomId: testGroup._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); + }); + }); + + it('should return messages that are pinned', 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(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); + }); + }); + }); + describe('/groups.invite', async () => { let roomInfo: { group: IRoom }; diff --git a/packages/rest-typings/src/v1/groups/GroupsMessageProps.ts b/packages/rest-typings/src/v1/groups/GroupsMessageProps.ts deleted file mode 100644 index aa92eafcf19d..000000000000 --- a/packages/rest-typings/src/v1/groups/GroupsMessageProps.ts +++ /dev/null @@ -1,32 +0,0 @@ -import Ajv from 'ajv'; - -import type { PaginatedRequest } from '../../helpers/PaginatedRequest'; -import type { GroupsBaseProps } from './BaseProps'; -import { withGroupBaseProperties } from './BaseProps'; - -const ajv = new Ajv({ - coerceTypes: true, -}); - -export type GroupsMessageProps = PaginatedRequest; - -const GroupsMessagePropsSchema = withGroupBaseProperties({ - count: { - type: 'number', - nullable: true, - }, - offset: { - type: 'number', - nullable: true, - }, - sort: { - type: 'string', - nullable: true, - }, - query: { - type: 'string', - nullable: true, - }, -}); - -export const isGroupsMessageProps = ajv.compile(GroupsMessagePropsSchema); diff --git a/packages/rest-typings/src/v1/groups/GroupsMessagesProps.ts b/packages/rest-typings/src/v1/groups/GroupsMessagesProps.ts new file mode 100644 index 000000000000..834aeea88e4a --- /dev/null +++ b/packages/rest-typings/src/v1/groups/GroupsMessagesProps.ts @@ -0,0 +1,50 @@ +import type { IRoom } from '@rocket.chat/core-typings'; +import Ajv from 'ajv'; + +import type { PaginatedRequest } from '../../helpers/PaginatedRequest'; +import { withGroupBaseProperties } from './BaseProps'; + +const ajv = new Ajv({ + coerceTypes: true, +}); + +export type GroupsMessagesProps = PaginatedRequest<{ + roomId: IRoom['_id']; + mentionIds?: string; + starredIds?: string; + pinned?: boolean; + query?: Record; +}>; + +const GroupsMessagesPropsSchema = withGroupBaseProperties({ + roomId: { + type: 'string', + }, + mentionIds: { + type: 'string', + }, + starredIds: { + type: 'string', + }, + pinned: { + type: 'string', + }, + count: { + type: 'number', + nullable: true, + }, + offset: { + type: 'number', + nullable: true, + }, + sort: { + type: 'string', + nullable: true, + }, + query: { + type: 'string', + nullable: true, + }, +}); + +export const isGroupsMessagesProps = ajv.compile(GroupsMessagesPropsSchema); diff --git a/packages/rest-typings/src/v1/groups/groups.ts b/packages/rest-typings/src/v1/groups/groups.ts index 529e086a81af..687418e7d4e9 100644 --- a/packages/rest-typings/src/v1/groups/groups.ts +++ b/packages/rest-typings/src/v1/groups/groups.ts @@ -20,7 +20,7 @@ import type { GroupsKickProps } from './GroupsKickProps'; import type { GroupsLeaveProps } from './GroupsLeaveProps'; import type { GroupsListProps } from './GroupsListProps'; import type { GroupsMembersProps } from './GroupsMembersProps'; -import type { GroupsMessageProps } from './GroupsMessageProps'; +import type { GroupsMessagesProps } from './GroupsMessagesProps'; import type { GroupsModeratorsProps } from './GroupsModeratorsProps'; import type { GroupsOnlineProps } from './GroupsOnlineProps'; import type { GroupsOpenProps } from './GroupsOpenProps'; @@ -99,7 +99,7 @@ export type GroupsEndpoints = { GET: (params: GroupsRolesProps) => { roles: IGetRoomRoles[] }; }; '/v1/groups.messages': { - GET: (params: GroupsMessageProps) => PaginatedResult<{ + GET: (params: GroupsMessagesProps) => PaginatedResult<{ messages: IMessage[]; }>; }; diff --git a/packages/rest-typings/src/v1/groups/index.ts b/packages/rest-typings/src/v1/groups/index.ts index 49907d3a08e5..12d08774cd28 100644 --- a/packages/rest-typings/src/v1/groups/index.ts +++ b/packages/rest-typings/src/v1/groups/index.ts @@ -10,7 +10,7 @@ export * from './GroupsFilesProps'; export * from './GroupsKickProps'; export * from './GroupsLeaveProps'; export * from './GroupsMembersProps'; -export * from './GroupsMessageProps'; +export * from './GroupsMessagesProps'; export * from './GroupsRolesProps'; export * from './GroupsUnarchiveProps'; export * from './GroupsAddAllProps';