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 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;