Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
6 changes: 6 additions & 0 deletions .changeset/flat-kiwis-joke.md
Original file line number Diff line number Diff line change
@@ -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
162 changes: 127 additions & 35 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,
isChatIgnoreUserProps,
isChatGetPinnedMessagesProps,
isChatGetMentionedMessagesProps,
Expand Down Expand Up @@ -247,6 +246,11 @@ type ChatUnpinMessage = {
messageId: IMessage['_id'];
};

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

const ChatPinMessageSchema = {
type: 'object',
properties: {
Expand All @@ -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<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 @@ -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.');
Expand All @@ -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
Expand Down Expand Up @@ -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<IMessage, 'rid'>, { previewUrls: this.bodyParams.previewUrls }),
);
const [message] = await normalizeMessagesForUser([sent], this.userId);

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

API.v1.addRoute(
Expand Down Expand Up @@ -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<IMessage, 'rid'>, { 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 },
Expand Down
75 changes: 4 additions & 71 deletions packages/rest-typings/src/v1/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<ChatSendMessage>(chatSendMessageSchema);

type ChatGetMessage = {
msgId: IMessage['_id'];
};
Expand Down Expand Up @@ -887,6 +816,10 @@ const ChatGetURLPreviewSchema = {
export const isChatGetURLPreviewProps = ajv.compile<ChatGetURLPreview>(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;
Expand Down