From 1cb495e4ab6104d90153416142e14e72ca7669eb Mon Sep 17 00:00:00 2001 From: Yashika soni Date: Tue, 10 Mar 2026 00:20:58 +0530 Subject: [PATCH 1/8] chore: migrate users.setStatus endpoint to new OpenAPI pattern with AJV validation --- packages/rest-typings/src/v1/users.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/rest-typings/src/v1/users.ts b/packages/rest-typings/src/v1/users.ts index 565620d31ba5e..2d45568adfde2 100644 --- a/packages/rest-typings/src/v1/users.ts +++ b/packages/rest-typings/src/v1/users.ts @@ -314,10 +314,6 @@ export type UsersEndpoints = { }; }; - '/v1/users.setStatus': { - POST: (params: { message?: string; status?: UserStatus; userId?: string; username?: string; user?: string }) => void; - }; - '/v1/users.getStatus': { GET: () => { status: 'online' | 'offline' | 'away' | 'busy'; From 21285f3028e00fef6c43814a9ca33ecec76bb02e Mon Sep 17 00:00:00 2001 From: Yashika soni Date: Tue, 10 Mar 2026 00:24:31 +0530 Subject: [PATCH 2/8] chore: migrate users.setStatus endpoint to new OpenAPI pattern with AJV validation --- apps/meteor/app/api/server/v1/users.ts | 193 +++++++++++++------------ 1 file changed, 102 insertions(+), 91 deletions(-) diff --git a/apps/meteor/app/api/server/v1/users.ts b/apps/meteor/app/api/server/v1/users.ts index 50c65abcd8d12..774d2bceb004a 100644 --- a/apps/meteor/app/api/server/v1/users.ts +++ b/apps/meteor/app/api/server/v1/users.ts @@ -21,6 +21,7 @@ import { ajv, validateBadRequestErrorResponse, validateUnauthorizedErrorResponse, + validateForbiddenErrorResponse } from '@rocket.chat/rest-typings'; import { getLoginExpirationInMs, wrapExceptions } from '@rocket.chat/tools'; import { Accounts } from 'meteor/accounts-base'; @@ -878,6 +879,107 @@ const usersEndpoints = API.v1 return API.v1.success({ suggestions }); }, + ) + .post( + 'users.setStatus', + { + authRequired: true, + rateLimiterOptions: { + numRequestsAllowed: 5, + intervalTimeInMS: 60000, + }, + body: ajv.compile<{ + status?: string; + message?: string; + userId?: string; + username?: string; + user?: string; + }>({ + type: 'object', + properties: { + status: { type: 'string' }, + message: { type: 'string' }, + userId: { type: 'string' }, + username: { type: 'string' }, + user: { type: 'string' }, + }, + anyOf: [ + { required: ['message'] }, + { required: ['status'] }, + ], + additionalProperties: false, + }), + response: { + 400: validateBadRequestErrorResponse, + 401: validateUnauthorizedErrorResponse, + 403: validateForbiddenErrorResponse, + 200: ajv.compile<{ success: boolean }>({ + type: 'object', + properties: { + success: { type: 'boolean', enum: [true] }, + }, + required: ['success'], + additionalProperties: false, + }), + }, + }, + async function action() { + if (!settings.get('Accounts_AllowUserStatusMessageChange')) { + return API.v1.failure('Change status is not allowed [error-not-allowed]', 'error-not-allowed', 400); + } + + const user = await (async () => { + if (isUserFromParams(this.bodyParams, this.userId, this.user)) { + return Users.findOneById(this.userId); + } + if (await hasPermissionAsync(this.userId, 'edit-other-user-info')) { + return getUserFromParams(this.bodyParams); + } + })(); + + if (!user) { + return API.v1.forbidden(); + } + + const { _id, username, roles, name } = user; + let { statusText, status } = user; + + if (this.bodyParams.message || this.bodyParams.message === '') { + await setStatusText(user, this.bodyParams.message, { emit: false }); + statusText = this.bodyParams.message; + } + + if (this.bodyParams.status) { + const validStatus = ['online', 'away', 'offline', 'busy']; + if (validStatus.includes(this.bodyParams.status)) { + status = this.bodyParams.status as UserStatus; + + if (status === 'offline' && !settings.get('Accounts_AllowInvisibleStatusOption')) { + throw new Meteor.Error('error-status-not-allowed', 'Invisible status is disabled', { + method: 'users.setStatus', + }); + } + + await Users.updateOne( + { _id: user._id }, + { $set: { status, statusDefault: status } }, + ); + + void wrapExceptions(() => Calendar.cancelUpcomingStatusChanges(user._id)).suppress(); + } else { + throw new Meteor.Error('error-invalid-status', 'Valid status types include online, away, offline, and busy.', { + method: 'users.setStatus', + }); + } + } + + void api.broadcast('presence.status', { + user: { status, _id, username, statusText, roles, name }, + previousStatus: user.status, + }); + + return API.v1.success(); + }, ); API.v1.addRoute( @@ -1426,97 +1528,6 @@ API.v1.addRoute( }, ); -API.v1.addRoute( - 'users.setStatus', - { - authRequired: true, - rateLimiterOptions: { - numRequestsAllowed: 5, - intervalTimeInMS: 60000, - }, - }, - { - async post() { - check( - this.bodyParams, - Match.OneOf( - Match.ObjectIncluding({ - status: Match.Maybe(String), - message: String, - }), - Match.ObjectIncluding({ - status: String, - message: Match.Maybe(String), - }), - ), - ); - - if (!settings.get('Accounts_AllowUserStatusMessageChange')) { - throw new Meteor.Error('error-not-allowed', 'Change status is not allowed', { - method: 'users.setStatus', - }); - } - - const user = await (async () => { - if (isUserFromParams(this.bodyParams, this.userId, this.user)) { - return Users.findOneById(this.userId); - } - if (await hasPermissionAsync(this.userId, 'edit-other-user-info')) { - return getUserFromParams(this.bodyParams); - } - })(); - - if (!user) { - return API.v1.forbidden(); - } - - const { _id, username, roles, name } = user; - let { statusText, status } = user; - - if (this.bodyParams.message || this.bodyParams.message === '') { - await setStatusText(user, this.bodyParams.message, { emit: false }); - statusText = this.bodyParams.message; - } - - if (this.bodyParams.status) { - const validStatus = ['online', 'away', 'offline', 'busy']; - if (validStatus.includes(this.bodyParams.status)) { - status = this.bodyParams.status; - - if (status === 'offline' && !settings.get('Accounts_AllowInvisibleStatusOption')) { - throw new Meteor.Error('error-status-not-allowed', 'Invisible status is disabled', { - method: 'users.setStatus', - }); - } - - await Users.updateOne( - { _id: user._id }, - { - $set: { - status, - statusDefault: status, - }, - }, - ); - - void wrapExceptions(() => Calendar.cancelUpcomingStatusChanges(user._id)).suppress(); - } else { - throw new Meteor.Error('error-invalid-status', 'Valid status types include online, away, offline, and busy.', { - method: 'users.setStatus', - }); - } - } - - void api.broadcast('presence.status', { - user: { status, _id, username, statusText, roles, name }, - previousStatus: user.status, - }); - - return API.v1.success(); - }, - }, -); - // status: 'online' | 'offline' | 'away' | 'busy'; // message?: string; // _id: string; From 681512acdea52412731d70788c09360325a41375 Mon Sep 17 00:00:00 2001 From: Yashika soni Date: Tue, 10 Mar 2026 00:25:20 +0530 Subject: [PATCH 3/8] Create migrate-users-setStatus-openapi.md --- .changeset/migrate-users-setStatus-openapi.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/migrate-users-setStatus-openapi.md diff --git a/.changeset/migrate-users-setStatus-openapi.md b/.changeset/migrate-users-setStatus-openapi.md new file mode 100644 index 0000000000000..f846582322a98 --- /dev/null +++ b/.changeset/migrate-users-setStatus-openapi.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": patch +"@rocket.chat/rest-typings": patch +--- + +Migrates the `users.setStatus` REST API endpoint from the legacy `API.v1.addRoute` pattern to the new chained `API.v1.post()` pattern with AJV request body and response schema validation and OpenAPI documentation support. From b9c6a9c00fb946d4921945c41f834d13394a8288 Mon Sep 17 00:00:00 2001 From: Yashika soni Date: Tue, 10 Mar 2026 01:03:04 +0530 Subject: [PATCH 4/8] Update migrate-users-setStatus-openapi.md --- .changeset/migrate-users-setStatus-openapi.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.changeset/migrate-users-setStatus-openapi.md b/.changeset/migrate-users-setStatus-openapi.md index f846582322a98..b9f11be318119 100644 --- a/.changeset/migrate-users-setStatus-openapi.md +++ b/.changeset/migrate-users-setStatus-openapi.md @@ -1,6 +1,6 @@ --- -"@rocket.chat/meteor": patch -"@rocket.chat/rest-typings": patch +"@rocket.chat/meteor": minor +"@rocket.chat/rest-typings": minor --- Migrates the `users.setStatus` REST API endpoint from the legacy `API.v1.addRoute` pattern to the new chained `API.v1.post()` pattern with AJV request body and response schema validation and OpenAPI documentation support. From 0a3f7120cd7edf1f10f6da70397241734357d039 Mon Sep 17 00:00:00 2001 From: Yashika soni Date: Tue, 10 Mar 2026 01:13:20 +0530 Subject: [PATCH 5/8] Update migrate-users-setStatus-openapi.md --- .changeset/migrate-users-setStatus-openapi.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.changeset/migrate-users-setStatus-openapi.md b/.changeset/migrate-users-setStatus-openapi.md index b9f11be318119..f846582322a98 100644 --- a/.changeset/migrate-users-setStatus-openapi.md +++ b/.changeset/migrate-users-setStatus-openapi.md @@ -1,6 +1,6 @@ --- -"@rocket.chat/meteor": minor -"@rocket.chat/rest-typings": minor +"@rocket.chat/meteor": patch +"@rocket.chat/rest-typings": patch --- Migrates the `users.setStatus` REST API endpoint from the legacy `API.v1.addRoute` pattern to the new chained `API.v1.post()` pattern with AJV request body and response schema validation and OpenAPI documentation support. From 1c82df82e7ba36161822f600628f8fdf309b0068 Mon Sep 17 00:00:00 2001 From: Yashika soni Date: Tue, 10 Mar 2026 10:16:36 +0530 Subject: [PATCH 6/8] Update users.ts --- apps/meteor/app/api/server/v1/users.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/apps/meteor/app/api/server/v1/users.ts b/apps/meteor/app/api/server/v1/users.ts index 774d2bceb004a..5abd7227f4967 100644 --- a/apps/meteor/app/api/server/v1/users.ts +++ b/apps/meteor/app/api/server/v1/users.ts @@ -897,8 +897,8 @@ const usersEndpoints = API.v1 }>({ type: 'object', properties: { - status: { type: 'string' }, - message: { type: 'string' }, + status: { type: 'string', minLength: 1 }, + message: { type: 'string', minLength: 1 }, userId: { type: 'string' }, username: { type: 'string' }, user: { type: 'string' }, @@ -949,7 +949,12 @@ const usersEndpoints = API.v1 statusText = this.bodyParams.message; } - if (this.bodyParams.status) { + if ('status' in this.bodyParams) { + if (!this.bodyParams.status) { + throw new Meteor.Error('error-invalid-status', 'Valid status types include online, away, offline, and busy.', { + method: 'users.setStatus', + }); + } const validStatus = ['online', 'away', 'offline', 'busy']; if (validStatus.includes(this.bodyParams.status)) { status = this.bodyParams.status as UserStatus; From d18f8751e6d04916509289d5526ec40dbf784986 Mon Sep 17 00:00:00 2001 From: Yashika soni Date: Tue, 10 Mar 2026 10:49:50 +0530 Subject: [PATCH 7/8] fix(users.setStatus): restore rest-typings entry --- packages/rest-typings/src/v1/users.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/rest-typings/src/v1/users.ts b/packages/rest-typings/src/v1/users.ts index 2d45568adfde2..565620d31ba5e 100644 --- a/packages/rest-typings/src/v1/users.ts +++ b/packages/rest-typings/src/v1/users.ts @@ -314,6 +314,10 @@ export type UsersEndpoints = { }; }; + '/v1/users.setStatus': { + POST: (params: { message?: string; status?: UserStatus; userId?: string; username?: string; user?: string }) => void; + }; + '/v1/users.getStatus': { GET: () => { status: 'online' | 'offline' | 'away' | 'busy'; From f45b12eef77e5f4b528abbf86e7e3214ef460b5b Mon Sep 17 00:00:00 2001 From: Yashika soni Date: Tue, 10 Mar 2026 10:49:52 +0530 Subject: [PATCH 8/8] fix(users.setStatus): allow empty message in AJV schema --- apps/meteor/app/api/server/v1/users.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/meteor/app/api/server/v1/users.ts b/apps/meteor/app/api/server/v1/users.ts index 5abd7227f4967..a2827440f79c3 100644 --- a/apps/meteor/app/api/server/v1/users.ts +++ b/apps/meteor/app/api/server/v1/users.ts @@ -898,7 +898,7 @@ const usersEndpoints = API.v1 type: 'object', properties: { status: { type: 'string', minLength: 1 }, - message: { type: 'string', minLength: 1 }, + message: { type: 'string' }, userId: { type: 'string' }, username: { type: 'string' }, user: { type: 'string' },