From 012455c62c6da7aac825f633ade7670295d75c4b Mon Sep 17 00:00:00 2001 From: ahmed-n-abdeltwab Date: Sun, 22 Feb 2026 22:09:07 +0200 Subject: [PATCH 1/4] feat: 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 to enhance API documentation and ensure type safety through response validation. --- apps/meteor/app/api/server/v1/chat.ts | 155 ++++++++++++++---- .../src/legacy/RocketchatSDKLegacy.ts | 30 ++-- .../ddp-client/src/legacy/types/SDKLegacy.ts | 4 +- packages/rest-typings/src/v1/chat.ts | 81 --------- 4 files changed, 141 insertions(+), 129 deletions(-) diff --git a/apps/meteor/app/api/server/v1/chat.ts b/apps/meteor/app/api/server/v1/chat.ts index f5a9250fe29b6..8e074b5568d46 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, isChatStarMessageProps, isChatUnstarMessageProps, isChatIgnoreUserProps, @@ -179,6 +178,11 @@ type ChatUnpinMessage = { messageId: IMessage['_id']; }; +type ChatSendMessage = { + message: Partial; + previewUrls?: string[]; +}; + const ChatPinMessageSchema = { type: 'object', properties: { @@ -203,10 +207,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'], + additionalProperties: false, +}; + const isChatPinMessageProps = ajv.compile(ChatPinMessageSchema); const isChatUnpinMessageProps = ajv.compile(ChatUnpinMessageSchema); +const isChatSendMessageProps = ajv.compile(chatSendMessageSchema); + const chatEndpoints = API.v1 .post( 'chat.pinMessage', @@ -303,20 +378,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.'); @@ -328,16 +403,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 @@ -346,6 +421,44 @@ const chatEndpoints = API.v1 const updatedMessage = await Messages.findOneById(msg._id); const [message] = await normalizeMessagesForUser(updatedMessage ? [updatedMessage] : [], this.userId); + return API.v1.success({ + message, + }); + }, + ) + .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, }); @@ -424,26 +537,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.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( 'chat.starMessage', diff --git a/packages/ddp-client/src/legacy/RocketchatSDKLegacy.ts b/packages/ddp-client/src/legacy/RocketchatSDKLegacy.ts index 93da57705e326..cfcbbe8394ed5 100644 --- a/packages/ddp-client/src/legacy/RocketchatSDKLegacy.ts +++ b/packages/ddp-client/src/legacy/RocketchatSDKLegacy.ts @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/no-this-alias */ import { RestClient } from '@rocket.chat/api-client'; -import type { IMessage, Serialized } from '@rocket.chat/core-typings'; +import type { Serialized } from '@rocket.chat/core-typings'; import { Emitter } from '@rocket.chat/emitter'; import type { OperationParams, OperationResult } from '@rocket.chat/rest-typings'; @@ -156,20 +156,20 @@ export class RocketchatSdkLegacyImpl extends DDPSDK implements RocketchatSDKLega return this.rest.post('/v1/im.create', { username }); } - sendMessage(message: IMessage | string, rid: string): Promise>> { - return this.rest.post('/v1/chat.sendMessage', { - message: - typeof message === 'string' - ? { - msg: message, - rid, - } - : { - ...message, - rid, - }, - }); - } + // sendMessage(message: IMessage | string, rid: string): Promise>> { + // return this.rest.post('/v1/chat.sendMessage', { + // message: + // typeof message === 'string' + // ? { + // msg: message, + // rid, + // } + // : { + // ...message, + // rid, + // }, + // }); + // } resume({ token }: { token: string }): Promise { return this.account.loginWithToken(token); diff --git a/packages/ddp-client/src/legacy/types/SDKLegacy.ts b/packages/ddp-client/src/legacy/types/SDKLegacy.ts index d3be02e85f4bb..02efd09bc9541 100644 --- a/packages/ddp-client/src/legacy/types/SDKLegacy.ts +++ b/packages/ddp-client/src/legacy/types/SDKLegacy.ts @@ -1,4 +1,4 @@ -import type { IMessage, Serialized } from '@rocket.chat/core-typings'; +import type { Serialized } from '@rocket.chat/core-typings'; import type { OperationParams, OperationResult } from '@rocket.chat/rest-typings'; import type { StreamerCallbackArgs } from '../../types/streams'; @@ -31,7 +31,7 @@ export interface APILegacy { createDirectMessage(username: string): Promise>>; - sendMessage(message: IMessage | string, rid: string): Promise>>; + // sendMessage(message: IMessage | string, rid: string): Promise>>; // getRoomIdByNameOrId(name: string): Promise>>; diff --git a/packages/rest-typings/src/v1/chat.ts b/packages/rest-typings/src/v1/chat.ts index 71fd745d7f95c..8a55a2ac7be8a 100644 --- a/packages/rest-typings/src/v1/chat.ts +++ b/packages/rest-typings/src/v1/chat.ts @@ -3,82 +3,6 @@ import type { IMessage, IRoom, MessageAttachment, IReadReceiptWithUser, MessageU import { ajv } from './Ajv'; import type { PaginatedRequest } from '../helpers/PaginatedRequest'; -type ChatSendMessage = { - message: Partial; - 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 ChatFollowMessage = { mid: IMessage['_id']; }; @@ -957,11 +881,6 @@ const ChatGetURLPreviewSchema = { export const isChatGetURLPreviewProps = ajv.compile(ChatGetURLPreviewSchema); export type ChatEndpoints = { - '/v1/chat.sendMessage': { - POST: (params: ChatSendMessage) => { - message: IMessage; - }; - }; '/v1/chat.getMessage': { GET: (params: ChatGetMessage) => { message: IMessage; From ca2fe32bc7579ce6749741597e22e436868c4063 Mon Sep 17 00:00:00 2001 From: Ahmed Nasser Date: Sun, 22 Feb 2026 22:13:05 +0200 Subject: [PATCH 2/4] Create flat-kiwis-joke.md --- .changeset/flat-kiwis-joke.md | 7 +++++++ 1 file changed, 7 insertions(+) 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..f4c2e9362defc --- /dev/null +++ b/.changeset/flat-kiwis-joke.md @@ -0,0 +1,7 @@ +--- +"@rocket.chat/meteor": patch +"@rocket.chat/ddp-client": patch +"@rocket.chat/rest-typings": patch +--- + +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 va From 4085de6c8c11718308ed1a7536cbaa2aa6a9d907 Mon Sep 17 00:00:00 2001 From: ahmed-n-abdeltwab Date: Mon, 23 Feb 2026 16:58:57 +0200 Subject: [PATCH 3/4] docs(api): relocate chat.sendMessage endpoint documentation comment - Update @rocket.chat/ui-contexts dependency from 27.0.0 to 27.0.1 in yarn.lock --- apps/meteor/app/api/server/v1/chat.ts | 6 +++--- yarn.lock | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/meteor/app/api/server/v1/chat.ts b/apps/meteor/app/api/server/v1/chat.ts index 8e074b5568d46..12f1f1203dee8 100644 --- a/apps/meteor/app/api/server/v1/chat.ts +++ b/apps/meteor/app/api/server/v1/chat.ts @@ -426,6 +426,9 @@ const chatEndpoints = API.v1 }); }, ) + // 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', { @@ -534,9 +537,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.starMessage', diff --git a/yarn.lock b/yarn.lock index a93a67032f4eb..f219de83d458d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10437,7 +10437,7 @@ __metadata: peerDependencies: "@rocket.chat/layout": "*" "@rocket.chat/tools": 0.2.4 - "@rocket.chat/ui-contexts": 27.0.0 + "@rocket.chat/ui-contexts": 27.0.1 "@tanstack/react-query": "*" react: "*" react-hook-form: "*" From 2f7935fb08f03a362500f4a05a5bc4a0b5ec8dec Mon Sep 17 00:00:00 2001 From: ahmed-n-abdeltwab Date: Mon, 23 Feb 2026 17:17:40 +0200 Subject: [PATCH 4/4] chore(api): remove extra blank line in chat.ts --- apps/meteor/app/api/server/v1/chat.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/meteor/app/api/server/v1/chat.ts b/apps/meteor/app/api/server/v1/chat.ts index 12f1f1203dee8..35775ff1bed51 100644 --- a/apps/meteor/app/api/server/v1/chat.ts +++ b/apps/meteor/app/api/server/v1/chat.ts @@ -537,7 +537,6 @@ API.v1.addRoute( }, ); - API.v1.addRoute( 'chat.starMessage', { authRequired: true, validateParams: isChatStarMessageProps },