From 03b63409211b0f0ca2c32e8ab01699424637b48b Mon Sep 17 00:00:00 2001 From: Mohamed Dawoud Date: Fri, 6 Mar 2026 16:14:07 +0200 Subject: [PATCH 1/3] chore: migrate `rooms.muteUser` and `rooms.unmuteUser` endpoints to the new pattern with AJV validation --- apps/meteor/app/api/server/v1/rooms.ts | 135 ++++++++++++++++++------- packages/rest-typings/src/v1/rooms.ts | 45 --------- 2 files changed, 98 insertions(+), 82 deletions(-) diff --git a/apps/meteor/app/api/server/v1/rooms.ts b/apps/meteor/app/api/server/v1/rooms.ts index 835c6cc5fd65b..3a48c3b24ef76 100644 --- a/apps/meteor/app/api/server/v1/rooms.ts +++ b/apps/meteor/app/api/server/v1/rooms.ts @@ -7,7 +7,6 @@ import { ajv, isGETRoomsNameExists, isRoomsImagesProps, - isRoomsMuteUnmuteUserProps, isRoomsExportProps, isRoomsIsMemberProps, isRoomsCleanHistoryProps, @@ -875,42 +874,6 @@ API.v1.addRoute( }, ); -API.v1.addRoute( - 'rooms.muteUser', - { authRequired: true, validateParams: isRoomsMuteUnmuteUserProps }, - { - async post() { - const user = await getUserFromParams(this.bodyParams); - - if (!user.username) { - return API.v1.failure('Invalid user'); - } - - await muteUserInRoom(this.userId, { rid: this.bodyParams.roomId, username: user.username }); - - return API.v1.success(); - }, - }, -); - -API.v1.addRoute( - 'rooms.unmuteUser', - { authRequired: true, validateParams: isRoomsMuteUnmuteUserProps }, - { - async post() { - const user = await getUserFromParams(this.bodyParams); - - if (!user.username) { - return API.v1.failure('Invalid user'); - } - - await unmuteUserInRoom(this.userId, { rid: this.bodyParams.roomId, username: user.username }); - - return API.v1.success(); - }, - }, -); - API.v1.addRoute( 'rooms.open', { authRequired: true, validateParams: isRoomsOpenProps }, @@ -1027,6 +990,44 @@ const isRoomsLeavePropsSchema = { const isRoomsFavoriteProps = ajv.compile(RoomsFavoriteSchema); const isRoomsLeaveProps = ajv.compile(isRoomsLeavePropsSchema); +type RoomsMuteUnmuteUser = { userId: string; roomId: string } | { username: string; roomId: string }; + +const RoomsMuteUnmuteUserSchema = { + type: 'object', + oneOf: [ + { + properties: { + userId: { + type: 'string', + minLength: 1, + }, + roomId: { + type: 'string', + minLength: 1, + }, + }, + required: ['userId', 'roomId'], + additionalProperties: false, + }, + { + properties: { + username: { + type: 'string', + minLength: 1, + }, + roomId: { + type: 'string', + minLength: 1, + }, + }, + required: ['username', 'roomId'], + additionalProperties: false, + }, + ], +}; + +const isRoomsMuteUnmuteUserProps = ajv.compile(RoomsMuteUnmuteUserSchema); + export const roomEndpoints = API.v1 .get( 'rooms.roles', @@ -1233,6 +1234,66 @@ export const roomEndpoints = API.v1 return API.v1.success(); }, + ) + .post( + 'rooms.muteUser', + { + authRequired: true, + body: isRoomsMuteUnmuteUserProps, + response: { + 200: ajv.compile<{ success: true }>({ + type: 'object', + properties: { + success: { type: 'boolean', enum: [true] }, + }, + required: ['success'], + additionalProperties: false, + }), + 400: validateBadRequestErrorResponse, + 401: validateUnauthorizedErrorResponse, + }, + }, + async function action() { + const user = await getUserFromParams(this.bodyParams); + + if (!user.username) { + return API.v1.failure('Invalid user'); + } + + await muteUserInRoom(this.userId, { rid: this.bodyParams.roomId, username: user.username }); + + return API.v1.success({ success: true }); + }, + ) + .post( + 'rooms.unmuteUser', + { + authRequired: true, + body: isRoomsMuteUnmuteUserProps, + response: { + 200: ajv.compile<{ success: true }>({ + type: 'object', + properties: { + success: { type: 'boolean', enum: [true] }, + }, + required: ['success'], + additionalProperties: false, + }), + 400: validateBadRequestErrorResponse, + 401: validateUnauthorizedErrorResponse, + }, + }, + async function action() { + const user = await getUserFromParams(this.bodyParams); + + if (!user.username) { + return API.v1.failure('Invalid user'); + } + + await unmuteUserInRoom(this.userId, { rid: this.bodyParams.roomId, username: user.username }); + + return API.v1.success({ success: true }); + }, ); type RoomEndpoints = ExtractRoutesFromAPI & diff --git a/packages/rest-typings/src/v1/rooms.ts b/packages/rest-typings/src/v1/rooms.ts index 97b8d483f2212..bc4694fdb7347 100644 --- a/packages/rest-typings/src/v1/rooms.ts +++ b/packages/rest-typings/src/v1/rooms.ts @@ -485,43 +485,6 @@ export type Notifications = { type RoomsGetDiscussionsProps = PaginatedRequest; -type RoomsMuteUnmuteUser = { userId: string; roomId: string } | { username: string; roomId: string }; - -const RoomsMuteUnmuteUserSchema = { - type: 'object', - oneOf: [ - { - properties: { - userId: { - type: 'string', - minLength: 1, - }, - roomId: { - type: 'string', - minLength: 1, - }, - }, - required: ['userId', 'roomId'], - additionalProperties: false, - }, - { - properties: { - username: { - type: 'string', - minLength: 1, - }, - roomId: { - type: 'string', - minLength: 1, - }, - }, - required: ['username', 'roomId'], - additionalProperties: false, - }, - ], -}; - -export const isRoomsMuteUnmuteUserProps = ajv.compile(RoomsMuteUnmuteUserSchema); export type RoomsImagesProps = { roomId: string; startingFromId?: string; @@ -823,14 +786,6 @@ export type RoomsEndpoints = { GET: (params: RoomsIsMemberProps) => { isMember: boolean }; }; - '/v1/rooms.muteUser': { - POST: (params: RoomsMuteUnmuteUser) => void; - }; - - '/v1/rooms.unmuteUser': { - POST: (params: RoomsMuteUnmuteUser) => void; - }; - '/v1/rooms.images': { GET: (params: RoomsImagesProps) => PaginatedResult<{ files: IUpload[]; From a221f309112a344ecd7f4b109dca796ea9bc12ef Mon Sep 17 00:00:00 2001 From: Mohamed Dawoud Date: Fri, 6 Mar 2026 20:36:15 +0200 Subject: [PATCH 2/3] docs: add changeset for room mute/unmute user migrations --- .changeset/migrate-rooms-mute-unmute-endpoints.md | 6 ++++++ apps/meteor/app/api/server/v1/rooms.ts | 8 ++++---- 2 files changed, 10 insertions(+), 4 deletions(-) create mode 100644 .changeset/migrate-rooms-mute-unmute-endpoints.md diff --git a/.changeset/migrate-rooms-mute-unmute-endpoints.md b/.changeset/migrate-rooms-mute-unmute-endpoints.md new file mode 100644 index 0000000000000..37437b3b92801 --- /dev/null +++ b/.changeset/migrate-rooms-mute-unmute-endpoints.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": minor +"@rocket.chat/rest-typings": minor +--- + +Migrated `rooms.muteUser` and `rooms.unmuteUser` endpoints to the new OpenAPI pattern with AJV schema validation diff --git a/apps/meteor/app/api/server/v1/rooms.ts b/apps/meteor/app/api/server/v1/rooms.ts index 3a48c3b24ef76..88650e48f4975 100644 --- a/apps/meteor/app/api/server/v1/rooms.ts +++ b/apps/meteor/app/api/server/v1/rooms.ts @@ -1241,7 +1241,7 @@ export const roomEndpoints = API.v1 authRequired: true, body: isRoomsMuteUnmuteUserProps, response: { - 200: ajv.compile<{ success: true }>({ + 200: ajv.compile({ type: 'object', properties: { success: { type: 'boolean', enum: [true] }, @@ -1262,7 +1262,7 @@ export const roomEndpoints = API.v1 await muteUserInRoom(this.userId, { rid: this.bodyParams.roomId, username: user.username }); - return API.v1.success({ success: true }); + return API.v1.success(); }, ) .post( @@ -1271,7 +1271,7 @@ export const roomEndpoints = API.v1 authRequired: true, body: isRoomsMuteUnmuteUserProps, response: { - 200: ajv.compile<{ success: true }>({ + 200: ajv.compile({ type: 'object', properties: { success: { type: 'boolean', enum: [true] }, @@ -1292,7 +1292,7 @@ export const roomEndpoints = API.v1 await unmuteUserInRoom(this.userId, { rid: this.bodyParams.roomId, username: user.username }); - return API.v1.success({ success: true }); + return API.v1.success(); }, ); From 9860f8a8120351d5ecd4f4456a10ef56c022b3e2 Mon Sep 17 00:00:00 2001 From: Mohamed Dawoud Date: Sat, 7 Mar 2026 16:43:20 +0200 Subject: [PATCH 3/3] refactor: merge room endpoints into single chain --- apps/meteor/app/api/server/v1/rooms.ts | 217 ++++++++++++------------- 1 file changed, 108 insertions(+), 109 deletions(-) diff --git a/apps/meteor/app/api/server/v1/rooms.ts b/apps/meteor/app/api/server/v1/rooms.ts index 88650e48f4975..e0e64ff23d78e 100644 --- a/apps/meteor/app/api/server/v1/rooms.ts +++ b/apps/meteor/app/api/server/v1/rooms.ts @@ -120,61 +120,6 @@ API.v1.addRoute( }, ); -const roomDeleteEndpoint = API.v1.post( - 'rooms.delete', - { - authRequired: true, - body: ajv.compile<{ roomId: string }>({ - type: 'object', - properties: { - roomId: { - type: 'string', - description: 'The ID of the room to delete.', - }, - }, - required: ['roomId'], - additionalProperties: false, - }), - response: { - 200: ajv.compile({ - type: 'object', - properties: { - success: { - type: 'boolean', - enum: [true], - description: 'Indicates if the request was successful.', - }, - }, - required: ['success'], - additionalProperties: false, - }), - 400: validateBadRequestErrorResponse, - 401: validateUnauthorizedErrorResponse, - }, - }, - async function action() { - const { roomId } = this.bodyParams; - - const room = await Rooms.findOneById(roomId); - - if (!room) { - throw new MeteorError('error-invalid-room', 'Invalid room', { - method: 'eraseRoom', - }); - } - - if (room.teamMain) { - throw new Meteor.Error('error-cannot-delete-team-channel', 'Cannot delete a team channel', { - method: 'eraseRoom', - }); - } - - await eraseRoom(room, this.user); - - return API.v1.success(); - }, -); - API.v1.addRoute( 'rooms.get', { authRequired: true }, @@ -305,56 +250,6 @@ API.v1.addRoute( }, ); -const saveNotificationBodySchema = ajv.compile<{ - roomId: string; - notifications: Record; -}>({ - type: 'object', - properties: { - roomId: { type: 'string', minLength: 1 }, - notifications: { - type: 'object', - minProperties: 1, - additionalProperties: { type: 'string' }, - }, - }, - required: ['roomId', 'notifications'], - additionalProperties: false, -}); - -const saveNotificationResponseSchema = ajv.compile({ - type: 'object', - properties: { - success: { type: 'boolean', enum: [true] }, - }, - required: ['success'], - additionalProperties: false, -}); - -const roomsSaveNotificationEndpoint = API.v1.post( - 'rooms.saveNotification', - { - authRequired: true, - body: saveNotificationBodySchema, - response: { - 200: saveNotificationResponseSchema, - 400: validateBadRequestErrorResponse, - 401: validateUnauthorizedErrorResponse, - }, - }, - async function action() { - const { roomId, notifications } = this.bodyParams; - - await Promise.all( - Object.entries(notifications as Notifications).map(async ([notificationKey, notificationValue]) => - saveNotificationSettingsMethod(this.userId, roomId, notificationKey as NotificationFieldType, notificationValue), - ), - ); - - return API.v1.success({ success: true }); - }, -); - API.v1.addRoute( 'rooms.cleanHistory', { authRequired: true, validateParams: isRoomsCleanHistoryProps }, @@ -1028,6 +923,36 @@ const RoomsMuteUnmuteUserSchema = { const isRoomsMuteUnmuteUserProps = ajv.compile(RoomsMuteUnmuteUserSchema); +type SaveNotificationBody = { + roomId: string; + notifications: Record; +}; + +const saveNotificationBodySchema = { + type: 'object', + properties: { + roomId: { type: 'string', minLength: 1 }, + notifications: { + type: 'object', + minProperties: 1, + additionalProperties: { type: 'string' }, + }, + }, + required: ['roomId', 'notifications'], + additionalProperties: false, +}; + +const isSaveNotificationBodyProps = ajv.compile(saveNotificationBodySchema); + +const saveNotificationResponseSchema = ajv.compile({ + type: 'object', + properties: { + success: { type: 'boolean', enum: [true] }, + }, + required: ['success'], + additionalProperties: false, +}); + export const roomEndpoints = API.v1 .get( 'rooms.roles', @@ -1171,6 +1096,29 @@ export const roomEndpoints = API.v1 } }, ) + .post( + 'rooms.saveNotification', + { + authRequired: true, + body: isSaveNotificationBodyProps, + response: { + 200: saveNotificationResponseSchema, + 400: validateBadRequestErrorResponse, + 401: validateUnauthorizedErrorResponse, + }, + }, + async function action() { + const { roomId, notifications } = this.bodyParams; + + await Promise.all( + Object.entries(notifications as Notifications).map(async ([notificationKey, notificationValue]) => + saveNotificationSettingsMethod(this.userId, roomId, notificationKey as NotificationFieldType, notificationValue), + ), + ); + + return API.v1.success({ success: true }); + }, + ) .post( 'rooms.favorite', { @@ -1203,6 +1151,60 @@ export const roomEndpoints = API.v1 return API.v1.success(); }, ) + .post( + 'rooms.delete', + { + authRequired: true, + body: ajv.compile<{ roomId: string }>({ + type: 'object', + properties: { + roomId: { + type: 'string', + description: 'The ID of the room to delete.', + }, + }, + required: ['roomId'], + additionalProperties: false, + }), + response: { + 200: ajv.compile({ + type: 'object', + properties: { + success: { + type: 'boolean', + enum: [true], + description: 'Indicates if the request was successful.', + }, + }, + required: ['success'], + additionalProperties: false, + }), + 400: validateBadRequestErrorResponse, + 401: validateUnauthorizedErrorResponse, + }, + }, + async function action() { + const { roomId } = this.bodyParams; + + const room = await Rooms.findOneById(roomId); + + if (!room) { + throw new MeteorError('error-invalid-room', 'Invalid room', { + method: 'eraseRoom', + }); + } + + if (room.teamMain) { + throw new Meteor.Error('error-cannot-delete-team-channel', 'Cannot delete a team channel', { + method: 'eraseRoom', + }); + } + + await eraseRoom(room, this.user); + + return API.v1.success(); + }, + ) .post( 'rooms.leave', { @@ -1296,10 +1298,7 @@ export const roomEndpoints = API.v1 }, ); -type RoomEndpoints = ExtractRoutesFromAPI & - ExtractRoutesFromAPI & - ExtractRoutesFromAPI & - ExtractRoutesFromAPI; +type RoomEndpoints = ExtractRoutesFromAPI; declare module '@rocket.chat/rest-typings' { // eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/no-empty-interface