Skip to content

Commit c1bbdea

Browse files
chore: Add OpenAPI support for the Rocket.Chat chat.sendMessage API endpoints
- migrating to a modern chained route definition syntax and utilizing shared AJV schemas for validation
1 parent 0d00b05 commit c1bbdea

File tree

3 files changed

+137
-106
lines changed

3 files changed

+137
-106
lines changed

.changeset/flat-kiwis-joke.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@rocket.chat/meteor": minor
3+
"@rocket.chat/rest-typings": minor
4+
---
5+
6+
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

apps/meteor/app/api/server/v1/chat.ts

Lines changed: 127 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import {
1313
isChatGetMessageProps,
1414
isChatPostMessageProps,
1515
isChatSearchProps,
16-
isChatSendMessageProps,
1716
isChatIgnoreUserProps,
1817
isChatGetPinnedMessagesProps,
1918
isChatGetMentionedMessagesProps,
@@ -247,6 +246,11 @@ type ChatUnpinMessage = {
247246
messageId: IMessage['_id'];
248247
};
249248

249+
type ChatSendMessage = {
250+
message: Partial<IMessage>;
251+
previewUrls?: string[];
252+
};
253+
250254
const ChatPinMessageSchema = {
251255
type: 'object',
252256
properties: {
@@ -271,10 +275,81 @@ const ChatUnpinMessageSchema = {
271275
additionalProperties: false,
272276
};
273277

278+
const chatSendMessageSchema = {
279+
type: 'object',
280+
properties: {
281+
message: {
282+
type: 'object',
283+
properties: {
284+
_id: {
285+
type: 'string',
286+
nullable: true,
287+
},
288+
rid: {
289+
type: 'string',
290+
},
291+
tmid: {
292+
type: 'string',
293+
nullable: true,
294+
},
295+
msg: {
296+
type: 'string',
297+
nullable: true,
298+
},
299+
alias: {
300+
type: 'string',
301+
nullable: true,
302+
},
303+
emoji: {
304+
type: 'string',
305+
nullable: true,
306+
},
307+
tshow: {
308+
type: 'boolean',
309+
nullable: true,
310+
},
311+
avatar: {
312+
type: 'string',
313+
nullable: true,
314+
},
315+
attachments: {
316+
type: 'array',
317+
items: {
318+
type: 'object',
319+
},
320+
nullable: true,
321+
},
322+
blocks: {
323+
type: 'array',
324+
items: {
325+
type: 'object',
326+
},
327+
nullable: true,
328+
},
329+
customFields: {
330+
type: 'object',
331+
nullable: true,
332+
},
333+
},
334+
},
335+
previewUrls: {
336+
type: 'array',
337+
items: {
338+
type: 'string',
339+
},
340+
nullable: true,
341+
},
342+
},
343+
required: ['message', 'rid'],
344+
additionalProperties: false,
345+
};
346+
274347
const isChatPinMessageProps = ajv.compile<ChatPinMessage>(ChatPinMessageSchema);
275348

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

351+
const isChatSendMessageProps = ajv.compile<ChatSendMessage>(chatSendMessageSchema);
352+
278353
const chatEndpoints = API.v1
279354
.post(
280355
'chat.pinMessage',
@@ -371,20 +446,20 @@ const chatEndpoints = API.v1
371446
},
372447
},
373448
async function action() {
374-
const { bodyParams } = this;
449+
const body = this.bodyParams;
375450

376-
const msg = await Messages.findOneById(bodyParams.msgId);
451+
const msg = await Messages.findOneById(body.msgId);
377452

378453
// Ensure the message exists
379454
if (!msg) {
380-
return API.v1.failure(`No message found with the id of "${bodyParams.msgId}".`);
455+
return API.v1.failure(`No message found with the id of "${body.msgId}".`);
381456
}
382457

383-
if (bodyParams.roomId !== msg.rid) {
458+
if (body.roomId !== msg.rid) {
384459
return API.v1.failure('The room id provided does not match where the message is from.');
385460
}
386461

387-
const hasContent = 'content' in bodyParams;
462+
const hasContent = 'content' in body;
388463

389464
if (hasContent && msg.t !== 'e2e') {
390465
return API.v1.failure('Only encrypted messages can have content updated.');
@@ -396,16 +471,16 @@ const chatEndpoints = API.v1
396471
? {
397472
_id: msg._id,
398473
rid: msg.rid,
399-
content: bodyParams.content,
400-
...(bodyParams.e2eMentions && { e2eMentions: bodyParams.e2eMentions }),
474+
content: body.content,
475+
...(body.e2eMentions && { e2eMentions: body.e2eMentions }),
401476
}
402477
: {
403478
_id: msg._id,
404479
rid: msg.rid,
405-
msg: bodyParams.text,
406-
...(bodyParams.customFields && { customFields: bodyParams.customFields }),
480+
msg: body.text,
481+
...(body.customFields && { customFields: body.customFields }),
407482
},
408-
'previewUrls' in bodyParams ? bodyParams.previewUrls : undefined,
483+
'previewUrls' in body ? body.previewUrls : undefined,
409484
];
410485

411486
// Permission checks are already done in the updateMessage method, so no need to duplicate them
@@ -558,6 +633,47 @@ const chatEndpoints = API.v1
558633

559634
return API.v1.success();
560635
},
636+
)
637+
// The difference between `chat.postMessage` and `chat.sendMessage` is that `chat.sendMessage` allows
638+
// for passing a value for `_id` and the other one doesn't. Also, `chat.sendMessage` only sends it to
639+
// one channel whereas the other one allows for sending to more than one channel at a time.
640+
.post(
641+
'chat.sendMessage',
642+
{
643+
authRequired: true,
644+
body: isChatSendMessageProps,
645+
response: {
646+
400: validateBadRequestErrorResponse,
647+
401: validateUnauthorizedErrorResponse,
648+
200: ajv.compile<{ message: IMessage }>({
649+
type: 'object',
650+
properties: {
651+
message: { $ref: '#/components/schemas/IMessage' },
652+
success: {
653+
type: 'boolean',
654+
enum: [true],
655+
},
656+
},
657+
required: ['message', 'success'],
658+
additionalProperties: false,
659+
}),
660+
},
661+
},
662+
663+
async function action() {
664+
if (MessageTypes.isSystemMessage(this.bodyParams.message)) {
665+
throw new Error("Cannot send system messages using 'chat.sendMessage'");
666+
}
667+
668+
const sent = await applyAirGappedRestrictionsValidation(() =>
669+
executeSendMessage(this.userId, this.bodyParams.message as Pick<IMessage, 'rid'>, { previewUrls: this.bodyParams.previewUrls }),
670+
);
671+
const [message] = await normalizeMessagesForUser([sent], this.userId);
672+
673+
return API.v1.success({
674+
message,
675+
});
676+
},
561677
);
562678

563679
API.v1.addRoute(
@@ -629,30 +745,6 @@ API.v1.addRoute(
629745
},
630746
);
631747

632-
// The difference between `chat.postMessage` and `chat.sendMessage` is that `chat.sendMessage` allows
633-
// for passing a value for `_id` and the other one doesn't. Also, `chat.sendMessage` only sends it to
634-
// one channel whereas the other one allows for sending to more than one channel at a time.
635-
API.v1.addRoute(
636-
'chat.sendMessage',
637-
{ authRequired: true, validateParams: isChatSendMessageProps },
638-
{
639-
async post() {
640-
if (MessageTypes.isSystemMessage(this.bodyParams.message)) {
641-
throw new Error("Cannot send system messages using 'chat.sendMessage'");
642-
}
643-
644-
const sent = await applyAirGappedRestrictionsValidation(() =>
645-
executeSendMessage(this.user, this.bodyParams.message as Pick<IMessage, 'rid'>, { previewUrls: this.bodyParams.previewUrls }),
646-
);
647-
const [message] = await normalizeMessagesForUser([sent], this.userId);
648-
649-
return API.v1.success({
650-
message,
651-
});
652-
},
653-
},
654-
);
655-
656748
API.v1.addRoute(
657749
'chat.react',
658750
{ authRequired: true, validateParams: isChatReactProps },

packages/rest-typings/src/v1/chat.ts

Lines changed: 4 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -8,77 +8,6 @@ type ChatSendMessage = {
88
previewUrls?: string[];
99
};
1010

11-
const chatSendMessageSchema = {
12-
type: 'object',
13-
properties: {
14-
message: {
15-
type: 'object',
16-
properties: {
17-
_id: {
18-
type: 'string',
19-
nullable: true,
20-
},
21-
rid: {
22-
type: 'string',
23-
},
24-
tmid: {
25-
type: 'string',
26-
nullable: true,
27-
},
28-
msg: {
29-
type: 'string',
30-
nullable: true,
31-
},
32-
alias: {
33-
type: 'string',
34-
nullable: true,
35-
},
36-
emoji: {
37-
type: 'string',
38-
nullable: true,
39-
},
40-
tshow: {
41-
type: 'boolean',
42-
nullable: true,
43-
},
44-
avatar: {
45-
type: 'string',
46-
nullable: true,
47-
},
48-
attachments: {
49-
type: 'array',
50-
items: {
51-
type: 'object',
52-
},
53-
nullable: true,
54-
},
55-
blocks: {
56-
type: 'array',
57-
items: {
58-
type: 'object',
59-
},
60-
nullable: true,
61-
},
62-
customFields: {
63-
type: 'object',
64-
nullable: true,
65-
},
66-
},
67-
},
68-
previewUrls: {
69-
type: 'array',
70-
items: {
71-
type: 'string',
72-
},
73-
nullable: true,
74-
},
75-
},
76-
required: ['message'],
77-
additionalProperties: false,
78-
};
79-
80-
export const isChatSendMessageProps = ajv.compile<ChatSendMessage>(chatSendMessageSchema);
81-
8211
type ChatGetMessage = {
8312
msgId: IMessage['_id'];
8413
};
@@ -887,6 +816,10 @@ const ChatGetURLPreviewSchema = {
887816
export const isChatGetURLPreviewProps = ajv.compile<ChatGetURLPreview>(ChatGetURLPreviewSchema);
888817

889818
export type ChatEndpoints = {
819+
// This workaround an issue where "@rocket.chat/ddp-client" does not reference
820+
// "@rocket.chat/meteor" correctly.
821+
// Without this, the 'sendMessage' endpoint becomes unrecognizable to
822+
// "@rocket.chat/ddp-client" and other dependent projects when moved or removed.
890823
'/v1/chat.sendMessage': {
891824
POST: (params: ChatSendMessage) => {
892825
message: IMessage;

0 commit comments

Comments
 (0)