diff --git a/.changeset/soft-peas-mate.md b/.changeset/soft-peas-mate.md new file mode 100644 index 0000000000000..6086511f69e61 --- /dev/null +++ b/.changeset/soft-peas-mate.md @@ -0,0 +1,6 @@ +--- +'@rocket.chat/rest-typings': minor +'@rocket.chat/meteor': minor +--- + +Migrated chat.getThreadsList existing old REST API structure to new typed REST (openAPI) structure diff --git a/apps/meteor/app/api/server/v1/chat.ts b/apps/meteor/app/api/server/v1/chat.ts index a00d57e46ae72..ee64ea1fef0db 100644 --- a/apps/meteor/app/api/server/v1/chat.ts +++ b/apps/meteor/app/api/server/v1/chat.ts @@ -1,13 +1,13 @@ import { Message } from '@rocket.chat/core-services'; -import type { IMessage, IThreadMainMessage } from '@rocket.chat/core-typings'; +import type { IMessage, IThreadMainMessage, IRoom } from '@rocket.chat/core-typings'; import { MessageTypes } from '@rocket.chat/message-types'; import { Messages, Users, Rooms, Subscriptions } from '@rocket.chat/models'; +import type { PaginatedRequest } from '@rocket.chat/rest-typings'; import { ajv, isChatReportMessageProps, isChatGetURLPreviewProps, isChatUpdateProps, - isChatGetThreadsListProps, isChatDeleteProps, isChatSyncMessagesProps, isChatGetMessageProps, @@ -275,6 +275,88 @@ const isChatPinMessageProps = ajv.compile(ChatPinMessageSchema); const isChatUnpinMessageProps = ajv.compile(ChatUnpinMessageSchema); +type ChatGetThreadsList = PaginatedRequest<{ + rid: IRoom['_id']; + type?: 'unread' | 'following'; + text?: string; + fields?: string; +}>; + +const ChatGetThreadsListSchema = { + type: 'object', + properties: { + rid: { + type: 'string', + }, + type: { + type: 'string', + enum: ['following', 'unread'], + nullable: true, + }, + text: { + type: 'string', + nullable: true, + }, + offset: { + type: 'number', + nullable: true, + }, + count: { + type: 'number', + nullable: true, + }, + sort: { + type: 'string', + nullable: true, + }, + query: { + type: 'string', + nullable: true, + }, + fields: { + type: 'string', + nullable: true, + }, + }, + required: ['rid'], + additionalProperties: false, +}; + +const isChatGetThreadsListLocalProps = ajv.compile(ChatGetThreadsListSchema); + +const isChatGetThreadsListResponse = ajv.compile<{ + threads: IMessage[]; + count: number; + offset: number; + total: number; + success: boolean; +}>({ + type: 'object', + properties: { + threads: { + type: 'array', + items: { + $ref: '#/components/schemas/IMessage', + }, + }, + count: { + type: 'number', + }, + offset: { + type: 'number', + }, + total: { + type: 'number', + }, + success: { + type: 'boolean', + enum: [true], + }, + }, + required: ['threads', 'count', 'offset', 'total', 'success'], + additionalProperties: false, +}); + const chatEndpoints = API.v1 .post( 'chat.pinMessage', @@ -558,6 +640,58 @@ const chatEndpoints = API.v1 return API.v1.success(); }, + ) + .get( + 'chat.getThreadsList', + { + authRequired: true, + query: isChatGetThreadsListLocalProps, + response: { + 400: validateBadRequestErrorResponse, + 401: validateUnauthorizedErrorResponse, + 200: isChatGetThreadsListResponse, + }, + }, + async function action() { + const { rid, type, text } = this.queryParams; + + const { offset, count } = await getPaginationItems(this.queryParams); + const { sort, fields, query } = await this.parseJsonQuery(); + + if (!settings.get('Threads_enabled')) { + throw new Meteor.Error('error-not-allowed', 'Threads Disabled'); + } + const user = await Users.findOneById(this.userId, { projection: { _id: 1 } }); + const room = await Rooms.findOneById(rid, { projection: { ...roomAccessAttributes, t: 1, _id: 1 } }); + + if (!room || !user || !(await canAccessRoomAsync(room, user))) { + throw new Meteor.Error('error-not-allowed', 'Not Allowed'); + } + + const typeThread = { + _hidden: { $ne: true }, + ...(type === 'following' && { replies: { $in: [this.userId] } }), + ...(type === 'unread' && { _id: { $in: (await Subscriptions.findOneByRoomIdAndUserId(room._id, user._id))?.tunread || [] } }), + msg: new RegExp(escapeRegExp(text || ''), 'i'), + }; + + const threadQuery = { ...query, ...typeThread, rid: room._id, tcount: { $exists: true } }; + const { cursor, totalCount } = await Messages.findPaginated(threadQuery, { + sort: sort || { tlm: -1 }, + skip: offset, + limit: count, + projection: fields, + }); + + const [threads, total] = await Promise.all([cursor.toArray(), totalCount]); + + return API.v1.success({ + threads: await normalizeMessagesForUser(threads, this.userId), + count: threads.length, + offset, + total, + }); + }, ); API.v1.addRoute( @@ -782,53 +916,6 @@ API.v1.addRoute( }, ); -API.v1.addRoute( - 'chat.getThreadsList', - { authRequired: true, validateParams: isChatGetThreadsListProps }, - { - async get() { - const { rid, type, text } = this.queryParams; - - const { offset, count } = await getPaginationItems(this.queryParams); - const { sort, fields, query } = await this.parseJsonQuery(); - - if (!settings.get('Threads_enabled')) { - throw new Meteor.Error('error-not-allowed', 'Threads Disabled'); - } - const user = await Users.findOneById(this.userId, { projection: { _id: 1 } }); - const room = await Rooms.findOneById(rid, { projection: { ...roomAccessAttributes, t: 1, _id: 1 } }); - - if (!room || !user || !(await canAccessRoomAsync(room, user))) { - throw new Meteor.Error('error-not-allowed', 'Not Allowed'); - } - - const typeThread = { - _hidden: { $ne: true }, - ...(type === 'following' && { replies: { $in: [this.userId] } }), - ...(type === 'unread' && { _id: { $in: (await Subscriptions.findOneByRoomIdAndUserId(room._id, user._id))?.tunread || [] } }), - msg: new RegExp(escapeRegExp(text || ''), 'i'), - }; - - const threadQuery = { ...query, ...typeThread, rid: room._id, tcount: { $exists: true } }; - const { cursor, totalCount } = await Messages.findPaginated(threadQuery, { - sort: sort || { tlm: -1 }, - skip: offset, - limit: count, - projection: fields, - }); - - const [threads, total] = await Promise.all([cursor.toArray(), totalCount]); - - return API.v1.success({ - threads: await normalizeMessagesForUser(threads, this.userId), - count: threads.length, - offset, - total, - }); - }, - }, -); - API.v1.addRoute( 'chat.syncThreadsList', { authRequired: true, validateParams: isChatSyncThreadsListProps }, @@ -1050,9 +1137,9 @@ API.v1.addRoute( }, ); + export type ChatEndpoints = ExtractRoutesFromAPI; declare module '@rocket.chat/rest-typings' { - // eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/no-empty-interface - interface Endpoints extends ChatEndpoints {} -} + interface Endpoints extends ChatEndpoints {} +} \ No newline at end of file diff --git a/apps/meteor/tests/end-to-end/api/chat.ts b/apps/meteor/tests/end-to-end/api/chat.ts index 7fd725ed5bfbc..124ddce87d51c 100644 --- a/apps/meteor/tests/end-to-end/api/chat.ts +++ b/apps/meteor/tests/end-to-end/api/chat.ts @@ -4111,7 +4111,7 @@ describe('Threads', () => { .expect(400) .expect((res) => { expect(res.body).to.have.property('success', false); - expect(res.body).to.have.property('errorType', 'invalid-params'); + expect(res.body).to.have.property('errorType', 'error-invalid-params'); }); }); @@ -4127,7 +4127,7 @@ describe('Threads', () => { .expect(400) .expect((res) => { expect(res.body).to.have.property('success', false); - expect(res.body).to.have.property('errorType', 'invalid-params'); + expect(res.body).to.have.property('errorType', 'error-invalid-params'); }); }); diff --git a/packages/rest-typings/src/v1/chat.ts b/packages/rest-typings/src/v1/chat.ts index f3a6e61f677b0..66de89872bbe0 100644 --- a/packages/rest-typings/src/v1/chat.ts +++ b/packages/rest-typings/src/v1/chat.ts @@ -1,4 +1,4 @@ -import type { IMessage, IRoom, MessageAttachment, IReadReceiptWithUser, MessageUrl, IThreadMainMessage } from '@rocket.chat/core-typings'; +import type { IMessage, IRoom, MessageAttachment, IReadReceiptWithUser, MessageUrl } from '@rocket.chat/core-typings'; import { ajv, ajvQuery } from './Ajv'; import type { PaginatedRequest } from '../helpers/PaginatedRequest'; @@ -146,55 +146,6 @@ const ChatReportMessageSchema = { export const isChatReportMessageProps = ajv.compile(ChatReportMessageSchema); -type ChatGetThreadsList = PaginatedRequest<{ - rid: IRoom['_id']; - type?: 'unread' | 'following'; - text?: string; - fields?: string; -}>; - -const ChatGetThreadsListSchema = { - type: 'object', - properties: { - rid: { - type: 'string', - }, - type: { - type: 'string', - enum: ['following', 'unread'], - nullable: true, - }, - text: { - type: 'string', - nullable: true, - }, - offset: { - type: 'number', - nullable: true, - }, - count: { - type: 'number', - nullable: true, - }, - sort: { - type: 'string', - nullable: true, - }, - query: { - type: 'string', - nullable: true, - }, - fields: { - type: 'string', - nullable: true, - }, - }, - required: ['rid'], - additionalProperties: false, -}; - -export const isChatGetThreadsListProps = ajvQuery.compile(ChatGetThreadsListSchema); - type ChatSyncThreadsList = { rid: IRoom['_id']; updatedSince: string; @@ -906,12 +857,6 @@ export type ChatEndpoints = { total: number; }; }; - '/v1/chat.getThreadsList': { - GET: (params: ChatGetThreadsList) => { - threads: IThreadMainMessage[]; - total: number; - }; - }; '/v1/chat.syncThreadsList': { GET: (params: ChatSyncThreadsList) => { threads: {