From cc3eb9d527fda3d6008523180948a4895e1855b8 Mon Sep 17 00:00:00 2001 From: sezallagwal Date: Tue, 3 Mar 2026 13:52:17 +0000 Subject: [PATCH 1/3] chore: migrate chat.delete and chat.react to OpenAPI --- .changeset/migrate-chat-delete-react.md | 6 + apps/meteor/app/api/server/v1/chat.ts | 217 +++++++++++++++++------- packages/rest-typings/src/v1/chat.ts | 84 +-------- 3 files changed, 160 insertions(+), 147 deletions(-) create mode 100644 .changeset/migrate-chat-delete-react.md diff --git a/.changeset/migrate-chat-delete-react.md b/.changeset/migrate-chat-delete-react.md new file mode 100644 index 0000000000000..6cce9984d9e52 --- /dev/null +++ b/.changeset/migrate-chat-delete-react.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": minor +"@rocket.chat/rest-typings": minor +--- + +Add OpenAPI support for the chat.delete and chat.react API endpoints by migrating to a modern chained route definition syntax and utilizing AJV schemas for body and response validation. diff --git a/apps/meteor/app/api/server/v1/chat.ts b/apps/meteor/app/api/server/v1/chat.ts index a00d57e46ae72..00b636e3d7cf1 100644 --- a/apps/meteor/app/api/server/v1/chat.ts +++ b/apps/meteor/app/api/server/v1/chat.ts @@ -8,7 +8,6 @@ import { isChatGetURLPreviewProps, isChatUpdateProps, isChatGetThreadsListProps, - isChatDeleteProps, isChatSyncMessagesProps, isChatGetMessageProps, isChatPostMessageProps, @@ -17,7 +16,6 @@ import { isChatIgnoreUserProps, isChatGetPinnedMessagesProps, isChatGetMentionedMessagesProps, - isChatReactProps, isChatGetDeletedMessagesProps, isChatSyncThreadsListProps, isChatGetThreadMessagesProps, @@ -127,46 +125,6 @@ const isChatFollowMessageLocalProps = ajv.compile(ChatFo const isChatUnfollowMessageLocalProps = ajv.compile(ChatUnfollowMessageLocalSchema); -API.v1.addRoute( - 'chat.delete', - { authRequired: true, validateParams: isChatDeleteProps }, - { - async post() { - const msg = await Messages.findOneById(this.bodyParams.msgId, { projection: { u: 1, rid: 1 } }); - - if (!msg) { - return API.v1.failure(`No message found with the id of "${this.bodyParams.msgId}".`); - } - - if (this.bodyParams.roomId !== msg.rid) { - return API.v1.failure('The room id provided does not match where the message is from.'); - } - - if ( - this.bodyParams.asUser && - msg.u._id !== this.userId && - !(await hasPermissionAsync(this.userId, 'force-delete-message', msg.rid)) - ) { - return API.v1.failure('Unauthorized. You must have the permission "force-delete-message" to delete other\'s message as them.'); - } - - const userId = this.bodyParams.asUser ? msg.u._id : this.userId; - const user = await Users.findOneById(userId, { projection: { _id: 1 } }); - - if (!user) { - return API.v1.failure('User not found'); - } - - await deleteMessageValidatingPermission(msg, user._id); - - return API.v1.success({ - _id: msg._id, - ts: Date.now().toString(), - message: msg, - }); - }, - }, -); API.v1.addRoute( 'chat.syncMessages', @@ -275,6 +233,56 @@ const isChatPinMessageProps = ajv.compile(ChatPinMessageSchema); const isChatUnpinMessageProps = ajv.compile(ChatUnpinMessageSchema); +type ChatDeleteLocal = { + msgId: string; + roomId: string; + asUser?: boolean; +}; + +const ChatDeleteLocalSchema = { + type: 'object', + properties: { + msgId: { type: 'string' }, + roomId: { type: 'string' }, + asUser: { type: 'boolean', nullable: true }, + }, + required: ['msgId', 'roomId'], + additionalProperties: false, +}; + +const isChatDeleteLocalProps = ajv.compile(ChatDeleteLocalSchema); + +type ChatReactLocal = + | { emoji: string; messageId: string; shouldReact?: boolean } + | { reaction: string; messageId: string; shouldReact?: boolean }; + +const ChatReactLocalSchema = { + oneOf: [ + { + type: 'object', + properties: { + emoji: { type: 'string' }, + messageId: { type: 'string', minLength: 1 }, + shouldReact: { type: 'boolean', nullable: true }, + }, + required: ['emoji', 'messageId'], + additionalProperties: false, + }, + { + type: 'object', + properties: { + reaction: { type: 'string' }, + messageId: { type: 'string', minLength: 1 }, + shouldReact: { type: 'boolean', nullable: true }, + }, + required: ['reaction', 'messageId'], + additionalProperties: false, + }, + ], +}; + +const isChatReactLocalProps = ajv.compile(ChatReactLocalSchema); + const chatEndpoints = API.v1 .post( 'chat.pinMessage', @@ -419,6 +427,109 @@ const chatEndpoints = API.v1 }); }, ) + .post( + 'chat.delete', + { + authRequired: true, + body: isChatDeleteLocalProps, + response: { + 400: validateBadRequestErrorResponse, + 401: validateUnauthorizedErrorResponse, + 200: ajv.compile<{ _id: string; ts: string; message: Pick }>({ + type: 'object', + properties: { + _id: { type: 'string' }, + ts: { type: 'string' }, + message: { + type: 'object', + properties: { + _id: { type: 'string' }, + rid: { type: 'string' }, + u: { type: 'object' }, + }, + required: ['_id', 'rid', 'u'], + }, + success: { type: 'boolean', enum: [true] }, + }, + required: ['_id', 'ts', 'message', 'success'], + additionalProperties: false, + }), + }, + }, + async function action() { + const msg = await Messages.findOneById(this.bodyParams.msgId, { projection: { u: 1, rid: 1 } }); + + if (!msg) { + return API.v1.failure(`No message found with the id of "${this.bodyParams.msgId}".`); + } + + if (this.bodyParams.roomId !== msg.rid) { + return API.v1.failure('The room id provided does not match where the message is from.'); + } + + if ( + this.bodyParams.asUser && + msg.u._id !== this.userId && + !(await hasPermissionAsync(this.userId, 'force-delete-message', msg.rid)) + ) { + return API.v1.failure('Unauthorized. You must have the permission "force-delete-message" to delete other\'s message as them.'); + } + + const userId = this.bodyParams.asUser ? msg.u._id : this.userId; + const user = await Users.findOneById(userId, { projection: { _id: 1 } }); + + if (!user) { + return API.v1.failure('User not found'); + } + + await deleteMessageValidatingPermission(msg, user._id); + + return API.v1.success({ + _id: msg._id, + ts: Date.now().toString(), + message: msg, + }); + }, + ) + .post( + 'chat.react', + { + authRequired: true, + body: isChatReactLocalProps, + response: { + 400: validateBadRequestErrorResponse, + 401: validateUnauthorizedErrorResponse, + 200: ajv.compile({ + type: 'object', + properties: { + success: { + type: 'boolean', + enum: [true], + }, + }, + required: ['success'], + additionalProperties: false, + }), + }, + }, + async function action() { + const msg = await Messages.findOneById(this.bodyParams.messageId); + + if (!msg) { + throw new Meteor.Error('error-message-not-found', 'The provided "messageId" does not match any existing message.'); + } + + const emoji = 'emoji' in this.bodyParams ? this.bodyParams.emoji : (this.bodyParams as { reaction: string }).reaction; + + if (!emoji) { + throw new Meteor.Error('error-emoji-param-not-provided', 'The required "emoji" param is missing.'); + } + + await executeSetReaction(this.userId, emoji, msg, this.bodyParams.shouldReact); + + return API.v1.success(); + }, + ) .post( 'chat.starMessage', { @@ -653,29 +764,7 @@ API.v1.addRoute( }, ); -API.v1.addRoute( - 'chat.react', - { authRequired: true, validateParams: isChatReactProps }, - { - async post() { - const msg = await Messages.findOneById(this.bodyParams.messageId); - - if (!msg) { - throw new Meteor.Error('error-message-not-found', 'The provided "messageId" does not match any existing message.'); - } - - const emoji = 'emoji' in this.bodyParams ? this.bodyParams.emoji : (this.bodyParams as { reaction: string }).reaction; - if (!emoji) { - throw new Meteor.Error('error-emoji-param-not-provided', 'The required "emoji" param is missing.'); - } - - await executeSetReaction(this.userId, emoji, msg, this.bodyParams.shouldReact); - - return API.v1.success(); - }, - }, -); API.v1.addRoute( 'chat.reportMessage', diff --git a/packages/rest-typings/src/v1/chat.ts b/packages/rest-typings/src/v1/chat.ts index b52bba2d61ed5..99db0871902ac 100644 --- a/packages/rest-typings/src/v1/chat.ts +++ b/packages/rest-typings/src/v1/chat.ts @@ -218,79 +218,6 @@ const ChatSyncThreadsListSchema = { export const isChatSyncThreadsListProps = ajv.compile(ChatSyncThreadsListSchema); -type ChatDelete = { - msgId: IMessage['_id']; - roomId: IRoom['_id']; - asUser?: boolean; -}; - -const ChatDeleteSchema = { - type: 'object', - properties: { - msgId: { - type: 'string', - }, - roomId: { - type: 'string', - }, - asUser: { - type: 'boolean', - nullable: true, - }, - }, - required: ['msgId', 'roomId'], - additionalProperties: false, -}; - -export const isChatDeleteProps = ajv.compile(ChatDeleteSchema); - -type ChatReact = - | { emoji: string; messageId: IMessage['_id']; shouldReact?: boolean } - | { reaction: string; messageId: IMessage['_id']; shouldReact?: boolean }; - -const ChatReactSchema = { - oneOf: [ - { - type: 'object', - properties: { - emoji: { - type: 'string', - }, - messageId: { - type: 'string', - minLength: 1, - }, - shouldReact: { - type: 'boolean', - nullable: true, - }, - }, - required: ['emoji', 'messageId'], - additionalProperties: false, - }, - { - type: 'object', - properties: { - reaction: { - type: 'string', - }, - messageId: { - type: 'string', - minLength: 1, - }, - shouldReact: { - type: 'boolean', - nullable: true, - }, - }, - required: ['reaction', 'messageId'], - additionalProperties: false, - }, - ], -}; - -export const isChatReactProps = ajv.compile(ChatReactSchema); - /** * The param `ignore` cannot be boolean, since this is a GET method. Use strings 'true' or 'false' instead. * @param {string} ignore @@ -920,16 +847,7 @@ export type ChatEndpoints = { }; }; }; - '/v1/chat.delete': { - POST: (params: ChatDelete) => { - _id: string; - ts: string; - message: Pick; - }; - }; - '/v1/chat.react': { - POST: (params: ChatReact) => void; - }; + '/v1/chat.ignoreUser': { GET: (params: ChatIgnoreUser) => void; }; From 3b1a9a595601c05287ba7343168caa39b985bc44 Mon Sep 17 00:00:00 2001 From: sezallagwal Date: Tue, 3 Mar 2026 14:27:22 +0000 Subject: [PATCH 2/3] fix /v1/chat.react endpoint typings --- packages/rest-typings/src/v1/chat.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/rest-typings/src/v1/chat.ts b/packages/rest-typings/src/v1/chat.ts index 99db0871902ac..952c341b11331 100644 --- a/packages/rest-typings/src/v1/chat.ts +++ b/packages/rest-typings/src/v1/chat.ts @@ -848,6 +848,9 @@ export type ChatEndpoints = { }; }; + '/v1/chat.react': { + POST: (params: { emoji: string; messageId: string; shouldReact?: boolean } | { reaction: string; messageId: string; shouldReact?: boolean }) => void; + }; '/v1/chat.ignoreUser': { GET: (params: ChatIgnoreUser) => void; }; From cb8f7fbc950d2a4e6664caca65a9228c6ec5a944 Mon Sep 17 00:00:00 2001 From: sezallagwal Date: Tue, 3 Mar 2026 16:01:42 +0000 Subject: [PATCH 3/3] fix: retain ChatReact type reference in ChatEndpoints --- apps/meteor/app/api/server/v1/chat.ts | 69 ++++++++++++++++++--------- packages/rest-typings/src/v1/chat.ts | 6 ++- 2 files changed, 51 insertions(+), 24 deletions(-) diff --git a/apps/meteor/app/api/server/v1/chat.ts b/apps/meteor/app/api/server/v1/chat.ts index 00b636e3d7cf1..91eee734f9544 100644 --- a/apps/meteor/app/api/server/v1/chat.ts +++ b/apps/meteor/app/api/server/v1/chat.ts @@ -1,5 +1,5 @@ import { Message } from '@rocket.chat/core-services'; -import type { IMessage, IThreadMainMessage } from '@rocket.chat/core-typings'; +import type { IMessage, IRoom, IThreadMainMessage } from '@rocket.chat/core-typings'; import { MessageTypes } from '@rocket.chat/message-types'; import { Messages, Users, Rooms, Subscriptions } from '@rocket.chat/models'; import { @@ -233,37 +233,50 @@ const isChatPinMessageProps = ajv.compile(ChatPinMessageSchema); const isChatUnpinMessageProps = ajv.compile(ChatUnpinMessageSchema); -type ChatDeleteLocal = { - msgId: string; - roomId: string; +type ChatDelete = { + msgId: IMessage['_id']; + roomId: IRoom['_id']; asUser?: boolean; }; -const ChatDeleteLocalSchema = { +const ChatDeleteSchema = { type: 'object', properties: { - msgId: { type: 'string' }, - roomId: { type: 'string' }, - asUser: { type: 'boolean', nullable: true }, + msgId: { + type: 'string', + }, + roomId: { + type: 'string', + }, + asUser: { + type: 'boolean', + nullable: true, + }, }, required: ['msgId', 'roomId'], additionalProperties: false, }; -const isChatDeleteLocalProps = ajv.compile(ChatDeleteLocalSchema); - -type ChatReactLocal = - | { emoji: string; messageId: string; shouldReact?: boolean } - | { reaction: string; messageId: string; shouldReact?: boolean }; +type ChatReact = + | { emoji: string; messageId: IMessage['_id']; shouldReact?: boolean } + | { reaction: string; messageId: IMessage['_id']; shouldReact?: boolean }; -const ChatReactLocalSchema = { +const ChatReactSchema = { oneOf: [ { type: 'object', properties: { - emoji: { type: 'string' }, - messageId: { type: 'string', minLength: 1 }, - shouldReact: { type: 'boolean', nullable: true }, + emoji: { + type: 'string', + }, + messageId: { + type: 'string', + minLength: 1, + }, + shouldReact: { + type: 'boolean', + nullable: true, + }, }, required: ['emoji', 'messageId'], additionalProperties: false, @@ -271,9 +284,17 @@ const ChatReactLocalSchema = { { type: 'object', properties: { - reaction: { type: 'string' }, - messageId: { type: 'string', minLength: 1 }, - shouldReact: { type: 'boolean', nullable: true }, + reaction: { + type: 'string', + }, + messageId: { + type: 'string', + minLength: 1, + }, + shouldReact: { + type: 'boolean', + nullable: true, + }, }, required: ['reaction', 'messageId'], additionalProperties: false, @@ -281,7 +302,9 @@ const ChatReactLocalSchema = { ], }; -const isChatReactLocalProps = ajv.compile(ChatReactLocalSchema); +const isChatDeleteProps = ajv.compile(ChatDeleteSchema); + +const isChatReactProps = ajv.compile(ChatReactSchema); const chatEndpoints = API.v1 .post( @@ -431,7 +454,7 @@ const chatEndpoints = API.v1 'chat.delete', { authRequired: true, - body: isChatDeleteLocalProps, + body: isChatDeleteProps, response: { 400: validateBadRequestErrorResponse, 401: validateUnauthorizedErrorResponse, @@ -495,7 +518,7 @@ const chatEndpoints = API.v1 'chat.react', { authRequired: true, - body: isChatReactLocalProps, + body: isChatReactProps, response: { 400: validateBadRequestErrorResponse, 401: validateUnauthorizedErrorResponse, diff --git a/packages/rest-typings/src/v1/chat.ts b/packages/rest-typings/src/v1/chat.ts index 952c341b11331..a21e8466cb915 100644 --- a/packages/rest-typings/src/v1/chat.ts +++ b/packages/rest-typings/src/v1/chat.ts @@ -218,6 +218,10 @@ const ChatSyncThreadsListSchema = { export const isChatSyncThreadsListProps = ajv.compile(ChatSyncThreadsListSchema); +type ChatReact = + | { emoji: string; messageId: IMessage['_id']; shouldReact?: boolean } + | { reaction: string; messageId: IMessage['_id']; shouldReact?: boolean }; + /** * The param `ignore` cannot be boolean, since this is a GET method. Use strings 'true' or 'false' instead. * @param {string} ignore @@ -849,7 +853,7 @@ export type ChatEndpoints = { }; '/v1/chat.react': { - POST: (params: { emoji: string; messageId: string; shouldReact?: boolean } | { reaction: string; messageId: string; shouldReact?: boolean }) => void; + POST: (params: ChatReact) => void; }; '/v1/chat.ignoreUser': { GET: (params: ChatIgnoreUser) => void;