Skip to content
Open
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
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'],
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