From c1bbdea182fb5d20e43b655e23dd2a01145ddfe5 Mon Sep 17 00:00:00 2001 From: ahmed-n-abdeltwab Date: Tue, 3 Mar 2026 13:52:59 +0200 Subject: [PATCH 1/2] chore: Add OpenAPI support for the Rocket.Chat chat.sendMessage API endpoints - migrating to a modern chained route definition syntax and utilizing shared AJV schemas for validation --- .changeset/flat-kiwis-joke.md | 6 + apps/meteor/app/api/server/v1/chat.ts | 162 ++++++++++++++++++++------ packages/rest-typings/src/v1/chat.ts | 75 +----------- 3 files changed, 137 insertions(+), 106 deletions(-) create mode 100644 .changeset/flat-kiwis-joke.md diff --git a/.changeset/flat-kiwis-joke.md b/.changeset/flat-kiwis-joke.md new file mode 100644 index 0000000000000..5d7f51ab19306 --- /dev/null +++ b/.changeset/flat-kiwis-joke.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": minor +"@rocket.chat/rest-typings": minor +--- + +Add OpenAPI support for the Rocket.Chat chat.sendMessage API endpoints by migrating to a modern chained route definition syntax and utilizing shared AJV schemas for validation diff --git a/apps/meteor/app/api/server/v1/chat.ts b/apps/meteor/app/api/server/v1/chat.ts index a00d57e46ae72..c3183a89a3319 100644 --- a/apps/meteor/app/api/server/v1/chat.ts +++ b/apps/meteor/app/api/server/v1/chat.ts @@ -13,7 +13,6 @@ import { isChatGetMessageProps, isChatPostMessageProps, isChatSearchProps, - isChatSendMessageProps, isChatIgnoreUserProps, isChatGetPinnedMessagesProps, isChatGetMentionedMessagesProps, @@ -247,6 +246,11 @@ type ChatUnpinMessage = { messageId: IMessage['_id']; }; +type ChatSendMessage = { + message: Partial; + previewUrls?: string[]; +}; + const ChatPinMessageSchema = { type: 'object', properties: { @@ -271,10 +275,81 @@ const ChatUnpinMessageSchema = { additionalProperties: false, }; +const chatSendMessageSchema = { + type: 'object', + properties: { + message: { + type: 'object', + properties: { + _id: { + type: 'string', + nullable: true, + }, + rid: { + type: 'string', + }, + tmid: { + type: 'string', + nullable: true, + }, + msg: { + type: 'string', + nullable: true, + }, + alias: { + type: 'string', + nullable: true, + }, + emoji: { + type: 'string', + nullable: true, + }, + tshow: { + type: 'boolean', + nullable: true, + }, + avatar: { + type: 'string', + nullable: true, + }, + attachments: { + type: 'array', + items: { + type: 'object', + }, + nullable: true, + }, + blocks: { + type: 'array', + items: { + type: 'object', + }, + nullable: true, + }, + customFields: { + type: 'object', + nullable: true, + }, + }, + }, + previewUrls: { + type: 'array', + items: { + type: 'string', + }, + nullable: true, + }, + }, + required: ['message', 'rid'], + additionalProperties: false, +}; + const isChatPinMessageProps = ajv.compile(ChatPinMessageSchema); const isChatUnpinMessageProps = ajv.compile(ChatUnpinMessageSchema); +const isChatSendMessageProps = ajv.compile(chatSendMessageSchema); + const chatEndpoints = API.v1 .post( 'chat.pinMessage', @@ -371,20 +446,20 @@ const chatEndpoints = API.v1 }, }, async function action() { - const { bodyParams } = this; + const body = this.bodyParams; - const msg = await Messages.findOneById(bodyParams.msgId); + const msg = await Messages.findOneById(body.msgId); // Ensure the message exists if (!msg) { - return API.v1.failure(`No message found with the id of "${bodyParams.msgId}".`); + return API.v1.failure(`No message found with the id of "${body.msgId}".`); } - if (bodyParams.roomId !== msg.rid) { + if (body.roomId !== msg.rid) { return API.v1.failure('The room id provided does not match where the message is from.'); } - const hasContent = 'content' in bodyParams; + const hasContent = 'content' in body; if (hasContent && msg.t !== 'e2e') { return API.v1.failure('Only encrypted messages can have content updated.'); @@ -396,16 +471,16 @@ const chatEndpoints = API.v1 ? { _id: msg._id, rid: msg.rid, - content: bodyParams.content, - ...(bodyParams.e2eMentions && { e2eMentions: bodyParams.e2eMentions }), + content: body.content, + ...(body.e2eMentions && { e2eMentions: body.e2eMentions }), } : { _id: msg._id, rid: msg.rid, - msg: bodyParams.text, - ...(bodyParams.customFields && { customFields: bodyParams.customFields }), + msg: body.text, + ...(body.customFields && { customFields: body.customFields }), }, - 'previewUrls' in bodyParams ? bodyParams.previewUrls : undefined, + 'previewUrls' in body ? body.previewUrls : undefined, ]; // Permission checks are already done in the updateMessage method, so no need to duplicate them @@ -558,6 +633,47 @@ const chatEndpoints = API.v1 return API.v1.success(); }, + ) + // The difference between `chat.postMessage` and `chat.sendMessage` is that `chat.sendMessage` allows + // for passing a value for `_id` and the other one doesn't. Also, `chat.sendMessage` only sends it to + // one channel whereas the other one allows for sending to more than one channel at a time. + .post( + 'chat.sendMessage', + { + authRequired: true, + body: isChatSendMessageProps, + response: { + 400: validateBadRequestErrorResponse, + 401: validateUnauthorizedErrorResponse, + 200: ajv.compile<{ message: IMessage }>({ + type: 'object', + properties: { + message: { $ref: '#/components/schemas/IMessage' }, + success: { + type: 'boolean', + enum: [true], + }, + }, + required: ['message', 'success'], + additionalProperties: false, + }), + }, + }, + + async function action() { + if (MessageTypes.isSystemMessage(this.bodyParams.message)) { + throw new Error("Cannot send system messages using 'chat.sendMessage'"); + } + + const sent = await applyAirGappedRestrictionsValidation(() => + executeSendMessage(this.userId, this.bodyParams.message as Pick, { previewUrls: this.bodyParams.previewUrls }), + ); + const [message] = await normalizeMessagesForUser([sent], this.userId); + + return API.v1.success({ + message, + }); + }, ); API.v1.addRoute( @@ -629,30 +745,6 @@ API.v1.addRoute( }, ); -// The difference between `chat.postMessage` and `chat.sendMessage` is that `chat.sendMessage` allows -// for passing a value for `_id` and the other one doesn't. Also, `chat.sendMessage` only sends it to -// one channel whereas the other one allows for sending to more than one channel at a time. -API.v1.addRoute( - 'chat.sendMessage', - { authRequired: true, validateParams: isChatSendMessageProps }, - { - async post() { - if (MessageTypes.isSystemMessage(this.bodyParams.message)) { - throw new Error("Cannot send system messages using 'chat.sendMessage'"); - } - - const sent = await applyAirGappedRestrictionsValidation(() => - executeSendMessage(this.user, this.bodyParams.message as Pick, { previewUrls: this.bodyParams.previewUrls }), - ); - const [message] = await normalizeMessagesForUser([sent], this.userId); - - return API.v1.success({ - message, - }); - }, - }, -); - API.v1.addRoute( 'chat.react', { authRequired: true, validateParams: isChatReactProps }, diff --git a/packages/rest-typings/src/v1/chat.ts b/packages/rest-typings/src/v1/chat.ts index b52bba2d61ed5..03f6e05174aaa 100644 --- a/packages/rest-typings/src/v1/chat.ts +++ b/packages/rest-typings/src/v1/chat.ts @@ -8,77 +8,6 @@ type ChatSendMessage = { previewUrls?: string[]; }; -const chatSendMessageSchema = { - type: 'object', - properties: { - message: { - type: 'object', - properties: { - _id: { - type: 'string', - nullable: true, - }, - rid: { - type: 'string', - }, - tmid: { - type: 'string', - nullable: true, - }, - msg: { - type: 'string', - nullable: true, - }, - alias: { - type: 'string', - nullable: true, - }, - emoji: { - type: 'string', - nullable: true, - }, - tshow: { - type: 'boolean', - nullable: true, - }, - avatar: { - type: 'string', - nullable: true, - }, - attachments: { - type: 'array', - items: { - type: 'object', - }, - nullable: true, - }, - blocks: { - type: 'array', - items: { - type: 'object', - }, - nullable: true, - }, - customFields: { - type: 'object', - nullable: true, - }, - }, - }, - previewUrls: { - type: 'array', - items: { - type: 'string', - }, - nullable: true, - }, - }, - required: ['message'], - additionalProperties: false, -}; - -export const isChatSendMessageProps = ajv.compile(chatSendMessageSchema); - type ChatGetMessage = { msgId: IMessage['_id']; }; @@ -887,6 +816,10 @@ const ChatGetURLPreviewSchema = { export const isChatGetURLPreviewProps = ajv.compile(ChatGetURLPreviewSchema); export type ChatEndpoints = { + // This workaround an issue where "@rocket.chat/ddp-client" does not reference + // "@rocket.chat/meteor" correctly. + // Without this, the 'sendMessage' endpoint becomes unrecognizable to + // "@rocket.chat/ddp-client" and other dependent projects when moved or removed. '/v1/chat.sendMessage': { POST: (params: ChatSendMessage) => { message: IMessage; From 9ae1f0bc0834be67ae53ccd6eb233fa692b297a9 Mon Sep 17 00:00:00 2001 From: ahmed-n-abdeltwab Date: Tue, 3 Mar 2026 17:49:30 +0200 Subject: [PATCH 2/2] chore: update chat.sendMessage schema to require only 'message' field --- apps/meteor/app/api/server/v1/chat.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/meteor/app/api/server/v1/chat.ts b/apps/meteor/app/api/server/v1/chat.ts index c3183a89a3319..97e861687cbea 100644 --- a/apps/meteor/app/api/server/v1/chat.ts +++ b/apps/meteor/app/api/server/v1/chat.ts @@ -340,7 +340,7 @@ const chatSendMessageSchema = { nullable: true, }, }, - required: ['message', 'rid'], + required: ['message'], additionalProperties: false, };