Skip to content

Commit 53a840d

Browse files
ggazzoclaude
andcommitted
chore: Migrate commands.run and commands.preview to OpenAPI
Convert commands.run, commands.preview GET and POST from addRoute to typed endpoints with AJV body/query validators and response schemas. Split commands.preview dual-method route into separate .get and .post calls. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 8684acc commit 53a840d

1 file changed

Lines changed: 195 additions & 136 deletions

File tree

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

Lines changed: 195 additions & 136 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
11
import { Apps } from '@rocket.chat/apps';
2-
import type { SlashCommand } from '@rocket.chat/core-typings';
2+
import type { SlashCommand, SlashCommandPreviewItem } from '@rocket.chat/core-typings';
33
import { Messages } from '@rocket.chat/models';
44
import { Random } from '@rocket.chat/random';
5-
import { ajv, validateUnauthorizedErrorResponse, validateBadRequestErrorResponse } from '@rocket.chat/rest-typings';
5+
import {
6+
ajv,
7+
validateUnauthorizedErrorResponse,
8+
validateBadRequestErrorResponse,
9+
validateForbiddenErrorResponse,
10+
} from '@rocket.chat/rest-typings';
611
import objectPath from 'object-path';
712

813
import { canAccessRoomIdAsync } from '../../../authorization/server/functions/canAccessRoom';
@@ -235,171 +240,225 @@ API.v1.addRoute(
235240
},
236241
);
237242

238-
// Expects a body of: { command: 'gimme', params: 'any string value', roomId: 'value', triggerId: 'value' }
239-
API.v1.addRoute(
240-
'commands.run',
241-
{ authRequired: true },
242-
{
243-
async post() {
244-
const body = this.bodyParams;
243+
const isCommandsRunProps = ajv.compile<{ command: string; params?: string; roomId: string; tmid?: string; triggerId: string }>({
244+
type: 'object',
245+
properties: {
246+
command: { type: 'string' },
247+
params: { type: 'string', nullable: true },
248+
roomId: { type: 'string' },
249+
tmid: { type: 'string', nullable: true },
250+
triggerId: { type: 'string' },
251+
},
252+
required: ['command', 'roomId', 'triggerId'],
253+
additionalProperties: false,
254+
});
245255

246-
if (typeof body.command !== 'string') {
247-
return API.v1.failure('You must provide a command to run.');
248-
}
256+
const commandsRunResponseSchema = ajv.compile<{ result: unknown }>({
257+
type: 'object',
258+
properties: {
259+
result: {},
260+
success: { type: 'boolean', enum: [true] },
261+
},
262+
required: ['success'],
263+
additionalProperties: true,
264+
});
249265

250-
if (body.params && typeof body.params !== 'string') {
251-
return API.v1.failure('The parameters for the command must be a single string.');
252-
}
266+
const isCommandsPreviewGetProps = ajv.compile<{ command: string; params?: string; roomId: string }>({
267+
type: 'object',
268+
properties: {
269+
command: { type: 'string' },
270+
params: { type: 'string', nullable: true },
271+
roomId: { type: 'string' },
272+
},
273+
required: ['command', 'roomId'],
274+
additionalProperties: false,
275+
});
253276

254-
if (typeof body.roomId !== 'string') {
255-
return API.v1.failure("The room's id where to execute this command must be provided and be a string.");
256-
}
277+
const commandsPreviewGetResponseSchema = ajv.compile<{ preview: Record<string, unknown> | undefined }>({
278+
type: 'object',
279+
properties: {
280+
preview: { type: 'object', nullable: true },
281+
success: { type: 'boolean', enum: [true] },
282+
},
283+
required: ['success'],
284+
additionalProperties: false,
285+
});
286+
287+
const isCommandsPreviewPostProps = ajv.compile<{
288+
command: string;
289+
params?: string;
290+
roomId: string;
291+
tmid?: string;
292+
triggerId?: string;
293+
previewItem: SlashCommandPreviewItem;
294+
}>({
295+
type: 'object',
296+
properties: {
297+
command: { type: 'string' },
298+
params: { type: 'string', nullable: true },
299+
roomId: { type: 'string' },
300+
tmid: { type: 'string', nullable: true },
301+
triggerId: { type: 'string', nullable: true },
302+
previewItem: {
303+
type: 'object',
304+
properties: {
305+
id: { type: 'string' },
306+
type: { type: 'string', enum: ['image', 'video', 'audio', 'text', 'other'] },
307+
value: { type: 'string' },
308+
},
309+
required: ['id', 'type', 'value'],
310+
additionalProperties: false,
311+
},
312+
},
313+
required: ['command', 'roomId', 'previewItem'],
314+
additionalProperties: false,
315+
});
257316

258-
if (body.tmid && typeof body.tmid !== 'string') {
259-
return API.v1.failure('The tmid parameter when provided must be a string.');
260-
}
317+
const commandsPreviewPostResponseSchema = ajv.compile<void>({
318+
type: 'object',
319+
properties: {
320+
success: { type: 'boolean', enum: [true] },
321+
},
322+
required: ['success'],
323+
additionalProperties: false,
324+
});
261325

262-
const cmd = body.command.toLowerCase();
263-
if (!slashCommands.commands[cmd]) {
264-
return API.v1.failure('The command provided does not exist (or is disabled).');
265-
}
326+
// Expects a body of: { command: 'gimme', params: 'any string value', roomId: 'value', triggerId: 'value' }
327+
API.v1.post(
328+
'commands.run',
329+
{
330+
authRequired: true,
331+
body: isCommandsRunProps,
332+
response: {
333+
200: commandsRunResponseSchema,
334+
400: validateBadRequestErrorResponse,
335+
401: validateUnauthorizedErrorResponse,
336+
403: validateForbiddenErrorResponse,
337+
},
338+
},
339+
async function action() {
340+
const body = this.bodyParams;
266341

267-
if (!(await canAccessRoomIdAsync(body.roomId, this.userId))) {
268-
return API.v1.forbidden();
269-
}
342+
const cmd = body.command.toLowerCase();
343+
if (!slashCommands.commands[cmd]) {
344+
return API.v1.failure('The command provided does not exist (or is disabled).');
345+
}
270346

271-
const params = body.params ? body.params : '';
272-
if (typeof body.tmid === 'string') {
273-
const thread = await Messages.findOneById(body.tmid);
274-
if (!thread || thread.rid !== body.roomId) {
275-
return API.v1.failure('Invalid thread.');
276-
}
347+
if (!(await canAccessRoomIdAsync(body.roomId, this.userId))) {
348+
return API.v1.forbidden('Not allowed');
349+
}
350+
351+
const params = body.params ? body.params : '';
352+
if (body.tmid) {
353+
const thread = await Messages.findOneById(body.tmid);
354+
if (thread?.rid !== body.roomId) {
355+
return API.v1.failure('Invalid thread.');
277356
}
357+
}
278358

279-
const message = {
280-
_id: Random.id(),
281-
rid: body.roomId,
282-
msg: `/${cmd} ${params}`,
283-
...(body.tmid && { tmid: body.tmid }),
284-
};
359+
const message = {
360+
_id: Random.id(),
361+
rid: body.roomId,
362+
msg: `/${cmd} ${params}`,
363+
...(body.tmid && { tmid: body.tmid }),
364+
};
285365

286-
const { triggerId } = body;
366+
const { triggerId } = body;
287367

288-
const result = await slashCommands.run({ command: cmd, params, message, triggerId, userId: this.userId });
368+
const result = await slashCommands.run({ command: cmd, params, message, triggerId, userId: this.userId });
289369

290-
return API.v1.success({ result });
291-
},
370+
return API.v1.success({ result });
292371
},
293372
);
294373

295-
API.v1.addRoute(
374+
// Expects these query params: command: 'giphy', params: 'mine', roomId: 'value'
375+
API.v1.get(
296376
'commands.preview',
297-
{ authRequired: true },
298377
{
299-
// Expects these query params: command: 'giphy', params: 'mine', roomId: 'value'
300-
async get() {
301-
const query = this.queryParams;
302-
303-
if (typeof query.command !== 'string') {
304-
return API.v1.failure('You must provide a command to get the previews from.');
305-
}
306-
307-
if (query.params && typeof query.params !== 'string') {
308-
return API.v1.failure('The parameters for the command must be a single string.');
309-
}
310-
311-
if (typeof query.roomId !== 'string') {
312-
return API.v1.failure("The room's id where the previews are being displayed must be provided and be a string.");
313-
}
314-
315-
const cmd = query.command.toLowerCase();
316-
if (!slashCommands.commands[cmd]) {
317-
return API.v1.failure('The command provided does not exist (or is disabled).');
318-
}
319-
320-
if (!(await canAccessRoomIdAsync(query.roomId, this.userId))) {
321-
return API.v1.forbidden();
322-
}
323-
324-
const params = query.params ? query.params : '';
325-
326-
const preview = await getSlashCommandPreviews({
327-
cmd,
328-
params,
329-
msg: { rid: query.roomId },
330-
userId: this.userId,
331-
});
332-
333-
return API.v1.success({ preview });
378+
authRequired: true,
379+
query: isCommandsPreviewGetProps,
380+
response: {
381+
200: commandsPreviewGetResponseSchema,
382+
400: validateBadRequestErrorResponse,
383+
401: validateUnauthorizedErrorResponse,
384+
403: validateForbiddenErrorResponse,
334385
},
386+
},
387+
async function action() {
388+
const query = this.queryParams;
335389

336-
// Expects a body format of: { command: 'giphy', params: 'mine', roomId: 'value', tmid: 'value', triggerId: 'value', previewItem: { id: 'sadf8' type: 'image', value: 'https://dev.null/gif' } }
337-
async post() {
338-
const body = this.bodyParams;
339-
340-
if (typeof body.command !== 'string') {
341-
return API.v1.failure('You must provide a command to run the preview item on.');
342-
}
390+
const cmd = query.command.toLowerCase();
391+
if (!slashCommands.commands[cmd]) {
392+
return API.v1.failure('The command provided does not exist (or is disabled).');
393+
}
343394

344-
if (body.params && typeof body.params !== 'string') {
345-
return API.v1.failure('The parameters for the command must be a single string.');
346-
}
395+
if (!(await canAccessRoomIdAsync(query.roomId, this.userId))) {
396+
return API.v1.forbidden('Not allowed');
397+
}
347398

348-
if (typeof body.roomId !== 'string') {
349-
return API.v1.failure("The room's id where the preview is being executed in must be provided and be a string.");
350-
}
399+
const params = query.params ? query.params : '';
351400

352-
if (typeof body.previewItem === 'undefined') {
353-
return API.v1.failure('The preview item being executed must be provided.');
354-
}
401+
const preview = await getSlashCommandPreviews({
402+
cmd,
403+
params,
404+
msg: { rid: query.roomId },
405+
userId: this.userId,
406+
});
355407

356-
if (!body.previewItem.id || !body.previewItem.type || typeof body.previewItem.value === 'undefined') {
357-
return API.v1.failure('The preview item being executed is in the wrong format.');
358-
}
408+
return API.v1.success({ preview });
409+
},
410+
);
359411

360-
if (body.tmid && typeof body.tmid !== 'string') {
361-
return API.v1.failure('The tmid parameter when provided must be a string.');
362-
}
412+
// Expects a body format of: { command: 'giphy', params: 'mine', roomId: 'value', tmid: 'value', triggerId: 'value', previewItem: { id: 'sadf8' type: 'image', value: 'https://dev.null/gif' } }
413+
API.v1.post(
414+
'commands.preview',
415+
{
416+
authRequired: true,
417+
body: isCommandsPreviewPostProps,
418+
response: {
419+
200: commandsPreviewPostResponseSchema,
420+
400: validateBadRequestErrorResponse,
421+
401: validateUnauthorizedErrorResponse,
422+
403: validateForbiddenErrorResponse,
423+
},
424+
},
425+
async function action() {
426+
const body = this.bodyParams;
363427

364-
if (body.triggerId && typeof body.triggerId !== 'string') {
365-
return API.v1.failure('The triggerId parameter when provided must be a string.');
366-
}
428+
const cmd = body.command.toLowerCase();
429+
if (!slashCommands.commands[cmd]) {
430+
return API.v1.failure('The command provided does not exist (or is disabled).');
431+
}
367432

368-
const cmd = body.command.toLowerCase();
369-
if (!slashCommands.commands[cmd]) {
370-
return API.v1.failure('The command provided does not exist (or is disabled).');
371-
}
433+
if (!(await canAccessRoomIdAsync(body.roomId, this.userId))) {
434+
return API.v1.forbidden('Not allowed');
435+
}
372436

373-
if (!(await canAccessRoomIdAsync(body.roomId, this.userId))) {
374-
return API.v1.forbidden();
437+
const { params = '' } = body;
438+
if (body.tmid) {
439+
const thread = await Messages.findOneById(body.tmid);
440+
if (thread?.rid !== body.roomId) {
441+
return API.v1.failure('Invalid thread.');
375442
}
443+
}
376444

377-
const { params = '' } = body;
378-
if (body.tmid) {
379-
const thread = await Messages.findOneById(body.tmid);
380-
if (!thread || thread.rid !== body.roomId) {
381-
return API.v1.failure('Invalid thread.');
382-
}
383-
}
445+
const msg = {
446+
rid: body.roomId,
447+
...(body.tmid && { tmid: body.tmid }),
448+
};
384449

385-
const msg = {
386-
rid: body.roomId,
387-
...(body.tmid && { tmid: body.tmid }),
388-
};
389-
390-
await executeSlashCommandPreview(
391-
{
392-
cmd,
393-
params,
394-
msg,
395-
triggerId: body.triggerId,
396-
},
397-
body.previewItem,
398-
this.userId,
399-
);
450+
await executeSlashCommandPreview(
451+
{
452+
cmd,
453+
params,
454+
msg,
455+
triggerId: body.triggerId,
456+
},
457+
body.previewItem,
458+
this.userId,
459+
);
400460

401-
return API.v1.success();
402-
},
461+
return API.v1.success();
403462
},
404463
);
405464

0 commit comments

Comments
 (0)