Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/flat-kiwis-joke.md
Original file line number Diff line number Diff line change
@@ -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
155 changes: 124 additions & 31 deletions apps/meteor/app/api/server/v1/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import {
isChatGetMessageProps,
isChatPostMessageProps,
isChatSearchProps,
isChatSendMessageProps,
isChatStarMessageProps,
isChatUnstarMessageProps,
isChatIgnoreUserProps,
Expand Down Expand Up @@ -179,6 +178,11 @@ type ChatUnpinMessage = {
messageId: IMessage['_id'];
};

type ChatSendMessage = {
message: Partial<IMessage>;
previewUrls?: string[];
};

const ChatPinMessageSchema = {
type: 'object',
properties: {
Expand All @@ -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<ChatPinMessage>(ChatPinMessageSchema);

const isChatUnpinMessageProps = ajv.compile<ChatUnpinMessage>(ChatUnpinMessageSchema);

const isChatSendMessageProps = ajv.compile<ChatSendMessage>(chatSendMessageSchema);

const chatEndpoints = API.v1
.post(
'chat.pinMessage',
Expand Down Expand Up @@ -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.');
Expand All @@ -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
Expand All @@ -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<IMessage, 'rid'>, { previewUrls: this.bodyParams.previewUrls }),
);
const [message] = await normalizeMessagesForUser([sent], this.userId);

return API.v1.success({
message,
});
Expand Down Expand Up @@ -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<IMessage, 'rid'>, { previewUrls: this.bodyParams.previewUrls }),
);
const [message] = await normalizeMessagesForUser([sent], this.userId);

return API.v1.success({
message,
});
},
},
);

API.v1.addRoute(
'chat.starMessage',
Expand Down
30 changes: 15 additions & 15 deletions packages/ddp-client/src/legacy/RocketchatSDKLegacy.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -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<Serialized<OperationResult<'POST', '/v1/chat.sendMessage'>>> {
return this.rest.post('/v1/chat.sendMessage', {
message:
typeof message === 'string'
? {
msg: message,
rid,
}
: {
...message,
rid,
},
});
}
// sendMessage(message: IMessage | string, rid: string): Promise<Serialized<OperationResult<'POST', '/v1/chat.sendMessage'>>> {
// return this.rest.post('/v1/chat.sendMessage', {
// message:
// typeof message === 'string'
// ? {
// msg: message,
// rid,
// }
// : {
// ...message,
// rid,
// },
// });
// }

resume({ token }: { token: string }): Promise<unknown> {
return this.account.loginWithToken(token);
Expand Down
4 changes: 2 additions & 2 deletions packages/ddp-client/src/legacy/types/SDKLegacy.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -31,7 +31,7 @@ export interface APILegacy {

createDirectMessage(username: string): Promise<Serialized<OperationResult<'POST', '/v1/im.create'>>>;

sendMessage(message: IMessage | string, rid: string): Promise<Serialized<OperationResult<'POST', '/v1/chat.sendMessage'>>>;
// sendMessage(message: IMessage | string, rid: string): Promise<Serialized<OperationResult<'POST', '/v1/chat.sendMessage'>>>;

// getRoomIdByNameOrId(name: string): Promise<Serialized<OperationResult<'GET', '/v1/chat.getRoomIdByNameOrId'>>>;

Expand Down
81 changes: 0 additions & 81 deletions packages/rest-typings/src/v1/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<IMessage>;
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<ChatSendMessage>(chatSendMessageSchema);

type ChatFollowMessage = {
mid: IMessage['_id'];
};
Expand Down Expand Up @@ -957,11 +881,6 @@ const ChatGetURLPreviewSchema = {
export const isChatGetURLPreviewProps = ajv.compile<ChatGetURLPreview>(ChatGetURLPreviewSchema);

export type ChatEndpoints = {
'/v1/chat.sendMessage': {
POST: (params: ChatSendMessage) => {
message: IMessage;
};
};
'/v1/chat.getMessage': {
GET: (params: ChatGetMessage) => {
message: IMessage;
Expand Down
Loading