From dbd7ec9732fe2092b8af929ba6127dbca78ae10f Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Mon, 23 Mar 2026 14:51:32 -0300 Subject: [PATCH 01/25] refactor(api): migrate chat.delete to typed endpoint with response schema Co-Authored-By: Claude Opus 4.6 (1M context) --- apps/meteor/app/api/server/v1/chat.ts | 106 ++++++++++++++++---------- 1 file changed, 65 insertions(+), 41 deletions(-) diff --git a/apps/meteor/app/api/server/v1/chat.ts b/apps/meteor/app/api/server/v1/chat.ts index 529aaa19a8ac3..a3011be57c143 100644 --- a/apps/meteor/app/api/server/v1/chat.ts +++ b/apps/meteor/app/api/server/v1/chat.ts @@ -127,47 +127,6 @@ const isChatFollowMessageLocalProps = ajv.compile(ChatFo const isChatUnfollowMessageLocalProps = ajv.compile(ChatUnfollowMessageLocalSchema); -API.v1.addRoute( - 'chat.delete', - { authRequired: true, validateParams: isChatDeleteProps }, - { - async post() { - const msg = await Messages.findOneById(this.bodyParams.msgId, { projection: { u: 1, rid: 1 } }); - - if (!msg) { - return API.v1.failure(`No message found with the id of "${this.bodyParams.msgId}".`); - } - - if (this.bodyParams.roomId !== msg.rid) { - return API.v1.failure('The room id provided does not match where the message is from.'); - } - - if ( - this.bodyParams.asUser && - msg.u._id !== this.userId && - !(await hasPermissionAsync(this.userId, 'force-delete-message', msg.rid)) - ) { - return API.v1.failure('Unauthorized. You must have the permission "force-delete-message" to delete other\'s message as them.'); - } - - const userId = this.bodyParams.asUser ? msg.u._id : this.userId; - const user = await Users.findOneById(userId, { projection: { _id: 1 } }); - - if (!user) { - return API.v1.failure('User not found'); - } - - await deleteMessageValidatingPermission(msg, user._id); - - return API.v1.success({ - _id: msg._id, - ts: Date.now().toString(), - message: msg, - }); - }, - }, -); - API.v1.addRoute( 'chat.syncMessages', { authRequired: true, validateParams: isChatSyncMessagesProps }, @@ -633,6 +592,71 @@ const chatEndpoints = API.v1 return API.v1.success(); }, + ) + .post( + 'chat.delete', + { + authRequired: true, + body: isChatDeleteProps, + response: { + 200: ajv.compile<{ _id: string; ts: string; message: Pick }>({ + type: 'object', + properties: { + _id: { type: 'string' }, + ts: { type: 'string' }, + message: { + type: 'object', + properties: { + _id: { type: 'string' }, + rid: { type: 'string' }, + u: { type: 'object' }, + }, + required: ['_id', 'rid', 'u'], + additionalProperties: true, + }, + success: { type: 'boolean', enum: [true] }, + }, + required: ['_id', 'ts', 'message', 'success'], + additionalProperties: false, + }), + 400: validateBadRequestErrorResponse, + 401: validateUnauthorizedErrorResponse, + }, + }, + async function action() { + const msg = await Messages.findOneById(this.bodyParams.msgId, { projection: { u: 1, rid: 1 } }); + + if (!msg) { + return API.v1.failure(`No message found with the id of "${this.bodyParams.msgId}".`); + } + + if (this.bodyParams.roomId !== msg.rid) { + return API.v1.failure('The room id provided does not match where the message is from.'); + } + + if ( + this.bodyParams.asUser && + msg.u._id !== this.userId && + !(await hasPermissionAsync(this.userId, 'force-delete-message', msg.rid)) + ) { + return API.v1.failure('Unauthorized. You must have the permission "force-delete-message" to delete other\'s message as them.'); + } + + const userId = this.bodyParams.asUser ? msg.u._id : this.userId; + const user = await Users.findOneById(userId, { projection: { _id: 1 } }); + + if (!user) { + return API.v1.failure('User not found'); + } + + await deleteMessageValidatingPermission(msg, user._id); + + return API.v1.success({ + _id: msg._id, + ts: Date.now().toString(), + message: msg, + }); + }, ); API.v1.addRoute( From 3ba0208d00c6043757385578db70b8f541e04b83 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Mon, 23 Mar 2026 14:53:40 -0300 Subject: [PATCH 02/25] refactor(api): migrate chat.syncMessages to typed endpoint with response schema Co-Authored-By: Claude Opus 4.6 (1M context) --- apps/meteor/app/api/server/v1/chat.ts | 118 ++++++++++++++++---------- 1 file changed, 74 insertions(+), 44 deletions(-) diff --git a/apps/meteor/app/api/server/v1/chat.ts b/apps/meteor/app/api/server/v1/chat.ts index a3011be57c143..5aa944b7add12 100644 --- a/apps/meteor/app/api/server/v1/chat.ts +++ b/apps/meteor/app/api/server/v1/chat.ts @@ -127,50 +127,6 @@ const isChatFollowMessageLocalProps = ajv.compile(ChatFo const isChatUnfollowMessageLocalProps = ajv.compile(ChatUnfollowMessageLocalSchema); -API.v1.addRoute( - 'chat.syncMessages', - { authRequired: true, validateParams: isChatSyncMessagesProps }, - { - async get() { - const { roomId, lastUpdate, count, next, previous, type } = this.queryParams; - - if (!roomId) { - throw new Meteor.Error('error-param-required', 'The required "roomId" query param is missing'); - } - - if (!lastUpdate && !type) { - throw new Meteor.Error('error-param-required', 'The "type" or "lastUpdate" parameters must be provided'); - } - - if (lastUpdate && isNaN(Date.parse(lastUpdate))) { - throw new Meteor.Error('error-lastUpdate-param-invalid', 'The "lastUpdate" query parameter must be a valid date'); - } - - const getMessagesQuery = { - ...(lastUpdate && { lastUpdate: new Date(lastUpdate) }), - ...(next && { next }), - ...(previous && { previous }), - ...(count && { count }), - ...(type && { type }), - }; - - const result = await getMessageHistory(roomId, this.userId, getMessagesQuery); - - if (!result) { - return API.v1.failure(); - } - - return API.v1.success({ - result: { - updated: 'updated' in result ? await normalizeMessagesForUser(result.updated, this.userId) : [], - deleted: 'deleted' in result ? result.deleted : [], - cursor: 'cursor' in result ? result.cursor : undefined, - }, - }); - }, - }, -); - API.v1.addRoute( 'chat.getMessage', { @@ -657,6 +613,80 @@ const chatEndpoints = API.v1 message: msg, }); }, + ) + .get( + 'chat.syncMessages', + { + authRequired: true, + query: isChatSyncMessagesProps, + response: { + 200: ajv.compile<{ + result: { updated: IMessage[]; deleted: IMessage[]; cursor?: { next: string | null; previous: string | null } }; + }>({ + type: 'object', + properties: { + result: { + type: 'object', + properties: { + updated: { type: 'array', items: { type: 'object' } }, + deleted: { type: 'array', items: { type: 'object' } }, + cursor: { + type: 'object', + properties: { + next: { type: ['string', 'null'] }, + previous: { type: ['string', 'null'] }, + }, + }, + }, + required: ['updated', 'deleted'], + additionalProperties: false, + }, + success: { type: 'boolean', enum: [true] }, + }, + required: ['result', 'success'], + additionalProperties: false, + }), + 400: validateBadRequestErrorResponse, + 401: validateUnauthorizedErrorResponse, + }, + }, + async function action() { + const { roomId, lastUpdate, count, next, previous, type } = this.queryParams; + + if (!roomId) { + throw new Meteor.Error('error-param-required', 'The required "roomId" query param is missing'); + } + + if (!lastUpdate && !type) { + throw new Meteor.Error('error-param-required', 'The "type" or "lastUpdate" parameters must be provided'); + } + + if (lastUpdate && isNaN(Date.parse(lastUpdate))) { + throw new Meteor.Error('error-lastUpdate-param-invalid', 'The "lastUpdate" query parameter must be a valid date'); + } + + const getMessagesQuery = { + ...(lastUpdate && { lastUpdate: new Date(lastUpdate) }), + ...(next && { next }), + ...(previous && { previous }), + ...(count && { count }), + ...(type && { type }), + }; + + const result = await getMessageHistory(roomId, this.userId, getMessagesQuery); + + if (!result) { + return API.v1.failure(); + } + + return API.v1.success({ + result: { + updated: 'updated' in result ? await normalizeMessagesForUser(result.updated, this.userId) : [], + deleted: 'deleted' in result ? result.deleted : [], + cursor: 'cursor' in result ? result.cursor : undefined, + }, + }); + }, ); API.v1.addRoute( From ab80b641b4dc52b695b90d2978f1c2a12668017d Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Mon, 23 Mar 2026 14:55:18 -0300 Subject: [PATCH 03/25] refactor(api): migrate chat.getMessage to typed endpoint with response schema Co-Authored-By: Claude Opus 4.6 (1M context) --- apps/meteor/app/api/server/v1/chat.ts | 64 ++++++++++++++++----------- 1 file changed, 37 insertions(+), 27 deletions(-) diff --git a/apps/meteor/app/api/server/v1/chat.ts b/apps/meteor/app/api/server/v1/chat.ts index 5aa944b7add12..a0c84de5772d1 100644 --- a/apps/meteor/app/api/server/v1/chat.ts +++ b/apps/meteor/app/api/server/v1/chat.ts @@ -127,33 +127,6 @@ const isChatFollowMessageLocalProps = ajv.compile(ChatFo const isChatUnfollowMessageLocalProps = ajv.compile(ChatUnfollowMessageLocalSchema); -API.v1.addRoute( - 'chat.getMessage', - { - authRequired: true, - validateParams: isChatGetMessageProps, - }, - { - async get() { - if (!this.queryParams.msgId) { - return API.v1.failure('The "msgId" query parameter must be provided.'); - } - - const msg = await getSingleMessage(this.userId, this.queryParams.msgId); - - if (!msg) { - return API.v1.failure(); - } - - const [message] = await normalizeMessagesForUser([msg], this.userId); - - return API.v1.success({ - message, - }); - }, - }, -); - type ChatPinMessage = { messageId: IMessage['_id']; }; @@ -687,6 +660,43 @@ const chatEndpoints = API.v1 }, }); }, + ) + .get( + 'chat.getMessage', + { + authRequired: true, + query: isChatGetMessageProps, + response: { + 200: ajv.compile<{ message: IMessage }>({ + type: 'object', + properties: { + message: { $ref: '#/components/schemas/IMessage' }, + success: { type: 'boolean', enum: [true] }, + }, + required: ['message', 'success'], + additionalProperties: false, + }), + 400: validateBadRequestErrorResponse, + 401: validateUnauthorizedErrorResponse, + }, + }, + async function action() { + if (!this.queryParams.msgId) { + return API.v1.failure('The "msgId" query parameter must be provided.'); + } + + const msg = await getSingleMessage(this.userId, this.queryParams.msgId); + + if (!msg) { + return API.v1.failure(); + } + + const [message] = await normalizeMessagesForUser([msg], this.userId); + + return API.v1.success({ + message, + }); + }, ); API.v1.addRoute( From 2a90012d7730ef7fb4d96d942c7d98453d2b60c4 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Mon, 23 Mar 2026 14:56:46 -0300 Subject: [PATCH 04/25] refactor(api): migrate chat.postMessage to typed endpoint with response schema Co-Authored-By: Claude Opus 4.6 (1M context) --- apps/meteor/app/api/server/v1/chat.ts | 33 +++++++++++++++++++-------- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/apps/meteor/app/api/server/v1/chat.ts b/apps/meteor/app/api/server/v1/chat.ts index a0c84de5772d1..23bcc36fa44cb 100644 --- a/apps/meteor/app/api/server/v1/chat.ts +++ b/apps/meteor/app/api/server/v1/chat.ts @@ -697,13 +697,29 @@ const chatEndpoints = API.v1 message, }); }, - ); - -API.v1.addRoute( - 'chat.postMessage', - { authRequired: true, validateParams: isChatPostMessageProps }, - { - async post() { + ) + .post( + 'chat.postMessage', + { + authRequired: true, + body: isChatPostMessageProps, + response: { + 200: ajv.compile<{ ts: number; channel: string; message: IMessage }>({ + type: 'object', + properties: { + ts: { type: 'number' }, + channel: { type: 'string' }, + message: { $ref: '#/components/schemas/IMessage' }, + success: { type: 'boolean', enum: [true] }, + }, + required: ['ts', 'channel', 'message', 'success'], + additionalProperties: false, + }), + 400: validateBadRequestErrorResponse, + 401: validateUnauthorizedErrorResponse, + }, + }, + async function action() { const { text, attachments } = this.bodyParams; const maxAllowedSize = settings.get('Message_MaxAllowedSize') ?? 0; @@ -733,8 +749,7 @@ API.v1.addRoute( message, }); }, - }, -); + ); API.v1.addRoute( 'chat.search', From 170192c4d06100fcd8c319ec47cb17364d02f8c8 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Mon, 23 Mar 2026 14:58:13 -0300 Subject: [PATCH 05/25] refactor(api): migrate chat.search to typed endpoint with response schema Co-Authored-By: Claude Opus 4.6 (1M context) --- apps/meteor/app/api/server/v1/chat.ts | 31 +++++++++++++++++++-------- 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/apps/meteor/app/api/server/v1/chat.ts b/apps/meteor/app/api/server/v1/chat.ts index 23bcc36fa44cb..e399964164d5d 100644 --- a/apps/meteor/app/api/server/v1/chat.ts +++ b/apps/meteor/app/api/server/v1/chat.ts @@ -749,13 +749,27 @@ const chatEndpoints = API.v1 message, }); }, - ); - -API.v1.addRoute( - 'chat.search', - { authRequired: true, validateParams: isChatSearchProps }, - { - async get() { + ) + .get( + 'chat.search', + { + authRequired: true, + query: isChatSearchProps, + response: { + 200: ajv.compile<{ messages: IMessage[] }>({ + type: 'object', + properties: { + messages: { type: 'array', items: { $ref: '#/components/schemas/IMessage' } }, + success: { type: 'boolean', enum: [true] }, + }, + required: ['messages', 'success'], + additionalProperties: false, + }), + 400: validateBadRequestErrorResponse, + 401: validateUnauthorizedErrorResponse, + }, + }, + async function action() { const { roomId, searchText } = this.queryParams; const { offset, count } = await getPaginationItems(this.queryParams); @@ -780,8 +794,7 @@ API.v1.addRoute( messages: await normalizeMessagesForUser(result, this.userId), }); }, - }, -); + ); // 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 From b49ef9b0ef0dd272a6303b6fcb9b5569e9fe78cc Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Mon, 23 Mar 2026 14:59:35 -0300 Subject: [PATCH 06/25] refactor(api): migrate chat.sendMessage to typed endpoint with response schema Co-Authored-By: Claude Opus 4.6 (1M context) --- apps/meteor/app/api/server/v1/chat.ts | 37 ++++++++++++++++++--------- 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/apps/meteor/app/api/server/v1/chat.ts b/apps/meteor/app/api/server/v1/chat.ts index e399964164d5d..95dd4ad71f08c 100644 --- a/apps/meteor/app/api/server/v1/chat.ts +++ b/apps/meteor/app/api/server/v1/chat.ts @@ -794,16 +794,30 @@ const chatEndpoints = API.v1 messages: await normalizeMessagesForUser(result, this.userId), }); }, - ); - -// 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() { + ) + // 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: { + 200: ajv.compile<{ message: IMessage }>({ + type: 'object', + properties: { + message: { $ref: '#/components/schemas/IMessage' }, + success: { type: 'boolean', enum: [true] }, + }, + required: ['message', 'success'], + additionalProperties: false, + }), + 400: validateBadRequestErrorResponse, + 401: validateUnauthorizedErrorResponse, + }, + }, + async function action() { if (MessageTypes.isSystemMessage(this.bodyParams.message)) { throw new Error("Cannot send system messages using 'chat.sendMessage'"); } @@ -817,8 +831,7 @@ API.v1.addRoute( message, }); }, - }, -); + ); API.v1.addRoute( 'chat.ignoreUser', From 2b68621ff40a33425e49eb8215ed1e22d45aeb73 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Mon, 23 Mar 2026 15:00:53 -0300 Subject: [PATCH 07/25] refactor(api): migrate chat.ignoreUser to typed endpoint with response schema Co-Authored-By: Claude Opus 4.6 (1M context) --- apps/meteor/app/api/server/v1/chat.ts | 30 +++++++++++++++++++-------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/apps/meteor/app/api/server/v1/chat.ts b/apps/meteor/app/api/server/v1/chat.ts index 95dd4ad71f08c..fb169ae499a15 100644 --- a/apps/meteor/app/api/server/v1/chat.ts +++ b/apps/meteor/app/api/server/v1/chat.ts @@ -831,13 +831,26 @@ const chatEndpoints = API.v1 message, }); }, - ); - -API.v1.addRoute( - 'chat.ignoreUser', - { authRequired: true, validateParams: isChatIgnoreUserProps }, - { - async get() { + ) + .get( + 'chat.ignoreUser', + { + authRequired: true, + query: isChatIgnoreUserProps, + response: { + 200: ajv.compile({ + type: 'object', + properties: { + success: { type: 'boolean', enum: [true] }, + }, + required: ['success'], + additionalProperties: false, + }), + 400: validateBadRequestErrorResponse, + 401: validateUnauthorizedErrorResponse, + }, + }, + async function action() { const { rid, userId } = this.queryParams; let { ignore = true } = this.queryParams; @@ -855,8 +868,7 @@ API.v1.addRoute( return API.v1.success(); }, - }, -); + ); API.v1.addRoute( 'chat.getDeletedMessages', From 2e98fcee5b7228a278904a8621507f5fa2b1dca9 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Mon, 23 Mar 2026 15:02:24 -0300 Subject: [PATCH 08/25] refactor(api): migrate chat.getDeletedMessages to typed endpoint with response schema Co-Authored-By: Claude Opus 4.6 (1M context) --- apps/meteor/app/api/server/v1/chat.ts | 34 ++++++++++++++++++++------- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/apps/meteor/app/api/server/v1/chat.ts b/apps/meteor/app/api/server/v1/chat.ts index fb169ae499a15..d91d25d7509f4 100644 --- a/apps/meteor/app/api/server/v1/chat.ts +++ b/apps/meteor/app/api/server/v1/chat.ts @@ -868,13 +868,30 @@ const chatEndpoints = API.v1 return API.v1.success(); }, - ); - -API.v1.addRoute( - 'chat.getDeletedMessages', - { authRequired: true, validateParams: isChatGetDeletedMessagesProps }, - { - async get() { + ) + .get( + 'chat.getDeletedMessages', + { + authRequired: true, + query: isChatGetDeletedMessagesProps, + response: { + 200: ajv.compile<{ messages: Pick[]; count: number; offset: number; total: number }>({ + type: 'object', + properties: { + messages: { type: 'array', items: { type: 'object' } }, + count: { type: 'number' }, + offset: { type: 'number' }, + total: { type: 'number' }, + success: { type: 'boolean', enum: [true] }, + }, + required: ['messages', 'count', 'offset', 'total', 'success'], + additionalProperties: false, + }), + 400: validateBadRequestErrorResponse, + 401: validateUnauthorizedErrorResponse, + }, + }, + async function action() { const { roomId, since } = this.queryParams; const { offset, count } = await getPaginationItems(this.queryParams); @@ -897,8 +914,7 @@ API.v1.addRoute( total, }); }, - }, -); + ); API.v1.addRoute( 'chat.getPinnedMessages', From 7d2ed6aad8c7d1d210a99db589b49a600fcad9d7 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Mon, 23 Mar 2026 15:03:54 -0300 Subject: [PATCH 09/25] refactor(api): migrate chat.getPinnedMessages to typed endpoint with response schema Co-Authored-By: Claude Opus 4.6 (1M context) --- apps/meteor/app/api/server/v1/chat.ts | 34 ++++++++++++++++++++------- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/apps/meteor/app/api/server/v1/chat.ts b/apps/meteor/app/api/server/v1/chat.ts index d91d25d7509f4..4c8881e164e92 100644 --- a/apps/meteor/app/api/server/v1/chat.ts +++ b/apps/meteor/app/api/server/v1/chat.ts @@ -914,13 +914,30 @@ const chatEndpoints = API.v1 total, }); }, - ); - -API.v1.addRoute( - 'chat.getPinnedMessages', - { authRequired: true, validateParams: isChatGetPinnedMessagesProps }, - { - async get() { + ) + .get( + 'chat.getPinnedMessages', + { + authRequired: true, + query: isChatGetPinnedMessagesProps, + response: { + 200: ajv.compile<{ messages: IMessage[]; count: number; offset: number; total: number }>({ + type: 'object', + properties: { + messages: { type: 'array', items: { $ref: '#/components/schemas/IMessage' } }, + count: { type: 'number' }, + offset: { type: 'number' }, + total: { type: 'number' }, + success: { type: 'boolean', enum: [true] }, + }, + required: ['messages', 'count', 'offset', 'total', 'success'], + additionalProperties: false, + }), + 400: validateBadRequestErrorResponse, + 401: validateUnauthorizedErrorResponse, + }, + }, + async function action() { const { roomId } = this.queryParams; const { offset, count } = await getPaginationItems(this.queryParams); @@ -942,8 +959,7 @@ API.v1.addRoute( total, }); }, - }, -); + ); API.v1.addRoute( 'chat.getThreadsList', From 5e21f63bc3b95af825b9bc178535ef9ac8c80e25 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Mon, 23 Mar 2026 15:05:49 -0300 Subject: [PATCH 10/25] refactor(api): migrate chat.getThreadsList to typed endpoint with response schema Co-Authored-By: Claude Opus 4.6 (1M context) --- apps/meteor/app/api/server/v1/chat.ts | 34 ++++++++++++++++++++------- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/apps/meteor/app/api/server/v1/chat.ts b/apps/meteor/app/api/server/v1/chat.ts index 4c8881e164e92..a5d456a9d73e0 100644 --- a/apps/meteor/app/api/server/v1/chat.ts +++ b/apps/meteor/app/api/server/v1/chat.ts @@ -959,13 +959,30 @@ const chatEndpoints = API.v1 total, }); }, - ); - -API.v1.addRoute( - 'chat.getThreadsList', - { authRequired: true, validateParams: isChatGetThreadsListProps }, - { - async get() { + ) + .get( + 'chat.getThreadsList', + { + authRequired: true, + query: isChatGetThreadsListProps, + response: { + 200: ajv.compile<{ threads: IThreadMainMessage[]; count: number; offset: number; total: number }>({ + type: 'object', + properties: { + threads: { type: 'array', items: { type: 'object' } }, + count: { type: 'number' }, + offset: { type: 'number' }, + total: { type: 'number' }, + success: { type: 'boolean', enum: [true] }, + }, + required: ['threads', 'count', 'offset', 'total', 'success'], + additionalProperties: false, + }), + 400: validateBadRequestErrorResponse, + 401: validateUnauthorizedErrorResponse, + }, + }, + async function action() { const { rid, type, text } = this.queryParams; const { offset, count } = await getPaginationItems(this.queryParams); @@ -1005,8 +1022,7 @@ API.v1.addRoute( total, }); }, - }, -); + ); API.v1.addRoute( 'chat.syncThreadsList', From 86b3aa1dd7f80f08a14543da402e84eaa7070152 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Mon, 23 Mar 2026 15:11:53 -0300 Subject: [PATCH 11/25] refactor(api): migrate chat.syncThreadsList to typed endpoint with response schema Co-Authored-By: Claude Opus 4.6 (1M context) --- apps/meteor/app/api/server/v1/chat.ts | 39 ++++++++++++++++++++------- 1 file changed, 30 insertions(+), 9 deletions(-) diff --git a/apps/meteor/app/api/server/v1/chat.ts b/apps/meteor/app/api/server/v1/chat.ts index a5d456a9d73e0..141e66fbbb37a 100644 --- a/apps/meteor/app/api/server/v1/chat.ts +++ b/apps/meteor/app/api/server/v1/chat.ts @@ -1022,13 +1022,35 @@ const chatEndpoints = API.v1 total, }); }, - ); - -API.v1.addRoute( - 'chat.syncThreadsList', - { authRequired: true, validateParams: isChatSyncThreadsListProps }, - { - async get() { + ) + .get( + 'chat.syncThreadsList', + { + authRequired: true, + query: isChatSyncThreadsListProps, + response: { + 200: ajv.compile<{ threads: { update: IMessage[]; remove: IMessage[] } }>({ + type: 'object', + properties: { + threads: { + type: 'object', + properties: { + update: { type: 'array', items: { type: 'object' } }, + remove: { type: 'array', items: { type: 'object' } }, + }, + required: ['update', 'remove'], + additionalProperties: false, + }, + success: { type: 'boolean', enum: [true] }, + }, + required: ['threads', 'success'], + additionalProperties: false, + }), + 400: validateBadRequestErrorResponse, + 401: validateUnauthorizedErrorResponse, + }, + }, + async function action() { const { rid } = this.queryParams; const { query, fields, sort } = await this.parseJsonQuery(); const { updatedSince } = this.queryParams; @@ -1065,8 +1087,7 @@ API.v1.addRoute( }, }); }, - }, -); + ); API.v1.addRoute( 'chat.getThreadMessages', From e2ace6a2300e07ad48592f9d2ef010cecfc33206 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Mon, 23 Mar 2026 15:13:23 -0300 Subject: [PATCH 12/25] refactor(api): migrate chat.getThreadMessages to typed endpoint with response schema Co-Authored-By: Claude Opus 4.6 (1M context) --- apps/meteor/app/api/server/v1/chat.ts | 34 ++++++++++++++++++++------- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/apps/meteor/app/api/server/v1/chat.ts b/apps/meteor/app/api/server/v1/chat.ts index 141e66fbbb37a..f6e0482a0a33e 100644 --- a/apps/meteor/app/api/server/v1/chat.ts +++ b/apps/meteor/app/api/server/v1/chat.ts @@ -1087,13 +1087,30 @@ const chatEndpoints = API.v1 }, }); }, - ); - -API.v1.addRoute( - 'chat.getThreadMessages', - { authRequired: true, validateParams: isChatGetThreadMessagesProps }, - { - async get() { + ) + .get( + 'chat.getThreadMessages', + { + authRequired: true, + query: isChatGetThreadMessagesProps, + response: { + 200: ajv.compile<{ messages: IMessage[]; count: number; offset: number; total: number }>({ + type: 'object', + properties: { + messages: { type: 'array', items: { type: 'object' } }, + count: { type: 'number' }, + offset: { type: 'number' }, + total: { type: 'number' }, + success: { type: 'boolean', enum: [true] }, + }, + required: ['messages', 'count', 'offset', 'total', 'success'], + additionalProperties: false, + }), + 400: validateBadRequestErrorResponse, + 401: validateUnauthorizedErrorResponse, + }, + }, + async function action() { const { tmid } = this.queryParams; const { query, fields, sort } = await this.parseJsonQuery(); const { offset, count } = await getPaginationItems(this.queryParams); @@ -1131,8 +1148,7 @@ API.v1.addRoute( total, }); }, - }, -); + ); API.v1.addRoute( 'chat.syncThreadMessages', From a159d395c9f6ccc5e49612106c88e4338400b063 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Mon, 23 Mar 2026 15:18:39 -0300 Subject: [PATCH 13/25] refactor(api): migrate chat.syncThreadMessages to typed endpoint with response schema Co-Authored-By: Claude Opus 4.6 (1M context) --- apps/meteor/app/api/server/v1/chat.ts | 39 ++++++++++++++++++++------- 1 file changed, 30 insertions(+), 9 deletions(-) diff --git a/apps/meteor/app/api/server/v1/chat.ts b/apps/meteor/app/api/server/v1/chat.ts index f6e0482a0a33e..8d04adaac2b83 100644 --- a/apps/meteor/app/api/server/v1/chat.ts +++ b/apps/meteor/app/api/server/v1/chat.ts @@ -1148,13 +1148,35 @@ const chatEndpoints = API.v1 total, }); }, - ); - -API.v1.addRoute( - 'chat.syncThreadMessages', - { authRequired: true, validateParams: isChatSyncThreadMessagesProps }, - { - async get() { + ) + .get( + 'chat.syncThreadMessages', + { + authRequired: true, + query: isChatSyncThreadMessagesProps, + response: { + 200: ajv.compile<{ messages: { update: IMessage[]; remove: IMessage[] } }>({ + type: 'object', + properties: { + messages: { + type: 'object', + properties: { + update: { type: 'array', items: { type: 'object' } }, + remove: { type: 'array', items: { type: 'object' } }, + }, + required: ['update', 'remove'], + additionalProperties: false, + }, + success: { type: 'boolean', enum: [true] }, + }, + required: ['messages', 'success'], + additionalProperties: false, + }), + 400: validateBadRequestErrorResponse, + 401: validateUnauthorizedErrorResponse, + }, + }, + async function action() { const { tmid } = this.queryParams; const { query, fields, sort } = await this.parseJsonQuery(); const { updatedSince } = this.queryParams; @@ -1186,8 +1208,7 @@ API.v1.addRoute( }, }); }, - }, -); + ); API.v1.addRoute( 'chat.getMentionedMessages', From 87b467e3a11e72437f88d24b6ab3ad796b825cbb Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Mon, 23 Mar 2026 15:20:08 -0300 Subject: [PATCH 14/25] refactor(api): migrate chat.getMentionedMessages to typed endpoint with response schema Co-Authored-By: Claude Opus 4.6 (1M context) --- apps/meteor/app/api/server/v1/chat.ts | 34 ++++++++++++++++++++------- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/apps/meteor/app/api/server/v1/chat.ts b/apps/meteor/app/api/server/v1/chat.ts index 8d04adaac2b83..6d172080e2712 100644 --- a/apps/meteor/app/api/server/v1/chat.ts +++ b/apps/meteor/app/api/server/v1/chat.ts @@ -1208,13 +1208,30 @@ const chatEndpoints = API.v1 }, }); }, - ); - -API.v1.addRoute( - 'chat.getMentionedMessages', - { authRequired: true, validateParams: isChatGetMentionedMessagesProps }, - { - async get() { + ) + .get( + 'chat.getMentionedMessages', + { + authRequired: true, + query: isChatGetMentionedMessagesProps, + response: { + 200: ajv.compile<{ messages: IMessage[]; count: number; offset: number; total: number }>({ + type: 'object', + properties: { + messages: { type: 'array', items: { type: 'object' } }, + count: { type: 'number' }, + offset: { type: 'number' }, + total: { type: 'number' }, + success: { type: 'boolean', enum: [true] }, + }, + required: ['messages', 'count', 'offset', 'total', 'success'], + additionalProperties: false, + }), + 400: validateBadRequestErrorResponse, + 401: validateUnauthorizedErrorResponse, + }, + }, + async function action() { const { roomId } = this.queryParams; const { sort } = await this.parseJsonQuery(); const { offset, count } = await getPaginationItems(this.queryParams); @@ -1231,8 +1248,7 @@ API.v1.addRoute( return API.v1.success(messages); }, - }, -); + ); API.v1.addRoute( 'chat.getStarredMessages', From 9d91af8790511b1a225b913c90a6574862e68573 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Mon, 23 Mar 2026 15:21:31 -0300 Subject: [PATCH 15/25] refactor(api): migrate chat.getStarredMessages to typed endpoint with response schema Co-Authored-By: Claude Opus 4.6 (1M context) --- apps/meteor/app/api/server/v1/chat.ts | 34 ++++++++++++++++++++------- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/apps/meteor/app/api/server/v1/chat.ts b/apps/meteor/app/api/server/v1/chat.ts index 6d172080e2712..bf3ef096a406b 100644 --- a/apps/meteor/app/api/server/v1/chat.ts +++ b/apps/meteor/app/api/server/v1/chat.ts @@ -1248,13 +1248,30 @@ const chatEndpoints = API.v1 return API.v1.success(messages); }, - ); - -API.v1.addRoute( - 'chat.getStarredMessages', - { authRequired: true, validateParams: isChatGetStarredMessagesProps }, - { - async get() { + ) + .get( + 'chat.getStarredMessages', + { + authRequired: true, + query: isChatGetStarredMessagesProps, + response: { + 200: ajv.compile<{ messages: IMessage[]; count: number; offset: number; total: number }>({ + type: 'object', + properties: { + messages: { type: 'array', items: { type: 'object' } }, + count: { type: 'number' }, + offset: { type: 'number' }, + total: { type: 'number' }, + success: { type: 'boolean', enum: [true] }, + }, + required: ['messages', 'count', 'offset', 'total', 'success'], + additionalProperties: false, + }), + 400: validateBadRequestErrorResponse, + 401: validateUnauthorizedErrorResponse, + }, + }, + async function action() { const { roomId } = this.queryParams; const { sort } = await this.parseJsonQuery(); const { offset, count } = await getPaginationItems(this.queryParams); @@ -1273,8 +1290,7 @@ API.v1.addRoute( return API.v1.success(messages); }, - }, -); + ); API.v1.addRoute( 'chat.getDiscussions', From 1a72de3372ae4232c380976e55ede2d76ab004c7 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Mon, 23 Mar 2026 15:22:59 -0300 Subject: [PATCH 16/25] refactor(api): migrate chat.getDiscussions to typed endpoint with response schema Co-Authored-By: Claude Opus 4.6 (1M context) --- apps/meteor/app/api/server/v1/chat.ts | 32 +++++++++++++++++++-------- 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/apps/meteor/app/api/server/v1/chat.ts b/apps/meteor/app/api/server/v1/chat.ts index bf3ef096a406b..b409ee6a62b36 100644 --- a/apps/meteor/app/api/server/v1/chat.ts +++ b/apps/meteor/app/api/server/v1/chat.ts @@ -1290,13 +1290,28 @@ const chatEndpoints = API.v1 return API.v1.success(messages); }, - ); - -API.v1.addRoute( - 'chat.getDiscussions', - { authRequired: true, validateParams: isChatGetDiscussionsProps }, - { - async get() { + ) + .get( + 'chat.getDiscussions', + { + authRequired: true, + query: isChatGetDiscussionsProps, + response: { + 200: ajv.compile<{ messages: IMessage[]; total: number }>({ + type: 'object', + properties: { + messages: { type: 'array', items: { type: 'object' } }, + total: { type: 'number' }, + success: { type: 'boolean', enum: [true] }, + }, + required: ['messages', 'total', 'success'], + additionalProperties: true, + }), + 400: validateBadRequestErrorResponse, + 401: validateUnauthorizedErrorResponse, + }, + }, + async function action() { const { roomId, text } = this.queryParams; const { sort } = await this.parseJsonQuery(); const { offset, count } = await getPaginationItems(this.queryParams); @@ -1313,8 +1328,7 @@ API.v1.addRoute( }); return API.v1.success(messages); }, - }, -); + ); API.v1.addRoute( 'chat.getURLPreview', From eea56d1944a3d0babaee361fe08e70aa4d7beaee Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Mon, 23 Mar 2026 15:24:18 -0300 Subject: [PATCH 17/25] refactor(api): migrate chat.getURLPreview to typed endpoint with response schema Co-Authored-By: Claude Opus 4.6 (1M context) --- apps/meteor/app/api/server/v1/chat.ts | 31 +++++++++++++++++++-------- 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/apps/meteor/app/api/server/v1/chat.ts b/apps/meteor/app/api/server/v1/chat.ts index b409ee6a62b36..2c9f645583670 100644 --- a/apps/meteor/app/api/server/v1/chat.ts +++ b/apps/meteor/app/api/server/v1/chat.ts @@ -1328,13 +1328,27 @@ const chatEndpoints = API.v1 }); return API.v1.success(messages); }, - ); - -API.v1.addRoute( - 'chat.getURLPreview', - { authRequired: true, validateParams: isChatGetURLPreviewProps }, - { - async get() { + ) + .get( + 'chat.getURLPreview', + { + authRequired: true, + query: isChatGetURLPreviewProps, + response: { + 200: ajv.compile<{ urlPreview: object }>({ + type: 'object', + properties: { + urlPreview: { type: 'object' }, + success: { type: 'boolean', enum: [true] }, + }, + required: ['urlPreview', 'success'], + additionalProperties: false, + }), + 400: validateBadRequestErrorResponse, + 401: validateUnauthorizedErrorResponse, + }, + }, + async function action() { const { roomId, url } = this.queryParams; if (!(await canAccessRoomIdAsync(roomId, this.userId))) { @@ -1346,8 +1360,7 @@ API.v1.addRoute( return API.v1.success({ urlPreview }); }, - }, -); + ); export type ChatEndpoints = ExtractRoutesFromAPI; From 92761b2598872e97e199fa50c514d76f28860c07 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Mon, 23 Mar 2026 15:46:23 -0300 Subject: [PATCH 18/25] refactor(api): migrate im.messages.others, im.list, im.list.everyone to typed endpoints Converts the last 3 addRoute calls in im.ts (each with dm/im aliases) to the typed .get() chain pattern with response schemas. Co-Authored-By: Claude Opus 4.6 (1M context) --- apps/meteor/app/api/server/v1/im.ts | 278 ++++++++++++++++------------ 1 file changed, 163 insertions(+), 115 deletions(-) diff --git a/apps/meteor/app/api/server/v1/im.ts b/apps/meteor/app/api/server/v1/im.ts index 21bd1f2df1cd9..44269c3ef6de0 100644 --- a/apps/meteor/app/api/server/v1/im.ts +++ b/apps/meteor/app/api/server/v1/im.ts @@ -743,6 +743,162 @@ const dmCreateResponseSchema = ajv.compile<{ room: IRoom & { rid: string } }>({ additionalProperties: false, }); +const paginatedMessagesResponseSchema = ajv.compile<{ messages: IMessage[]; offset: number; count: number; total: number }>({ + type: 'object', + properties: { + messages: { type: 'array', items: { type: 'object' } }, + offset: { type: 'number' }, + count: { type: 'number' }, + total: { type: 'number' }, + success: { type: 'boolean', enum: [true] }, + }, + required: ['messages', 'offset', 'count', 'total', 'success'], + additionalProperties: false, +}); + +const paginatedImsResponseSchema = ajv.compile<{ ims: IRoom[]; offset: number; count: number; total: number }>({ + type: 'object', + properties: { + ims: { type: 'array', items: { type: 'object' } }, + offset: { type: 'number' }, + count: { type: 'number' }, + total: { type: 'number' }, + success: { type: 'boolean', enum: [true] }, + }, + required: ['ims', 'offset', 'count', 'total', 'success'], + additionalProperties: false, +}); + +const dmMessagesOthersEndpointsProps = { + authRequired: true as const, + permissionsRequired: ['view-room-administration'], + response: { + 200: paginatedMessagesResponseSchema, + 400: validateBadRequestErrorResponse, + 401: validateUnauthorizedErrorResponse, + 403: validateForbiddenErrorResponse, + }, +}; + +const dmMessagesOthersAction = (_name: Path): TypedAction => + async function action() { + if (settings.get('API_Enable_Direct_Message_History_EndPoint') !== true) { + throw new Meteor.Error('error-endpoint-disabled', 'This endpoint is disabled', { + route: '/api/v1/im.messages.others', + }); + } + + const { roomId } = this.queryParams; + if (!roomId) { + throw new Meteor.Error('error-roomid-param-not-provided', 'The parameter "roomId" is required'); + } + + const room = await Rooms.findOneById>(roomId, { projection: { _id: 1, t: 1 } }); + if (!room || room?.t !== 'd') { + throw new Meteor.Error('error-room-not-found', `No direct message room found by the id of: ${roomId}`); + } + + const { offset, count } = await getPaginationItems(this.queryParams); + const { sort, fields, query } = await this.parseJsonQuery(); + const ourQuery = Object.assign({}, query, { rid: room._id }); + + const { cursor, totalCount } = Messages.findPaginated(ourQuery, { + sort: sort || { ts: -1 }, + skip: offset, + limit: count, + projection: fields, + }); + + const [msgs, total] = await Promise.all([cursor.toArray(), totalCount]); + + if (!msgs) { + throw new Meteor.Error('error-no-messages', 'No messages found'); + } + + return API.v1.success({ + messages: await normalizeMessagesForUser(msgs, this.userId), + offset, + count: msgs.length, + total, + }); + }; + +const dmListEndpointsProps = { + authRequired: true as const, + response: { + 200: paginatedImsResponseSchema, + 400: validateBadRequestErrorResponse, + 401: validateUnauthorizedErrorResponse, + }, +}; + +const dmListAction = (_name: Path): TypedAction => + async function action() { + const { offset, count } = await getPaginationItems(this.queryParams); + const { sort = { name: 1 }, fields } = await this.parseJsonQuery(); + + // TODO: CACHE: Add Breaking notice since we removed the query param + + const subscriptions = await Subscriptions.find({ 'u._id': this.userId, 't': 'd' }, { projection: { rid: 1 } }) + .map((item) => item.rid) + .toArray(); + + const { cursor, totalCount } = Rooms.findPaginated( + { t: 'd', _id: { $in: subscriptions } }, + { + sort, + skip: offset, + limit: count, + projection: fields, + }, + ); + + const [ims, total] = await Promise.all([cursor.toArray(), totalCount]); + + return API.v1.success({ + ims: await Promise.all(ims.map((room: IRoom) => composeRoomWithLastMessage(room, this.userId))), + offset, + count: ims.length, + total, + }); + }; + +const dmListEveryoneEndpointsProps = { + authRequired: true as const, + permissionsRequired: ['view-room-administration'], + response: { + 200: paginatedImsResponseSchema, + 400: validateBadRequestErrorResponse, + 401: validateUnauthorizedErrorResponse, + 403: validateForbiddenErrorResponse, + }, +}; + +const dmListEveryoneAction = (_name: Path): TypedAction => + async function action() { + const { offset, count }: { offset: number; count: number } = await getPaginationItems(this.queryParams); + const { sort, fields, query } = await this.parseJsonQuery(); + + const { cursor, totalCount } = Rooms.findPaginated( + { ...query, t: 'd' }, + { + sort: sort || { name: 1 }, + skip: offset, + limit: count, + projection: fields, + }, + ); + + const [rooms, total] = await Promise.all([cursor.toArray(), totalCount]); + + return API.v1.success({ + ims: await Promise.all(rooms.map((room: IRoom) => composeRoomWithLastMessage(room, this.userId))), + offset, + count: rooms.length, + total, + }); + }; + const dmEndpoints = API.v1 .post('im.delete', dmDeleteEndpointsProps, dmDeleteAction('im.delete')) .post('dm.delete', dmDeleteEndpointsProps, dmDeleteAction('dm.delete')) @@ -809,121 +965,13 @@ const dmEndpoints = API.v1 .get('dm.messages', dmMessagesEndpointsProps, dmMessagesAction('dm.messages')) .get('im.messages', dmMessagesEndpointsProps, dmMessagesAction('im.messages')) .get('dm.history', dmHistoryEndpointsProps, dmHistoryAction('dm.history')) - .get('im.history', dmHistoryEndpointsProps, dmHistoryAction('im.history')); - -API.v1.addRoute( - ['dm.messages.others', 'im.messages.others'], - { authRequired: true, permissionsRequired: ['view-room-administration'] }, - { - async get() { - if (settings.get('API_Enable_Direct_Message_History_EndPoint') !== true) { - throw new Meteor.Error('error-endpoint-disabled', 'This endpoint is disabled', { - route: '/api/v1/im.messages.others', - }); - } - - const { roomId } = this.queryParams; - if (!roomId) { - throw new Meteor.Error('error-roomid-param-not-provided', 'The parameter "roomId" is required'); - } - - const room = await Rooms.findOneById>(roomId, { projection: { _id: 1, t: 1 } }); - if (!room || room?.t !== 'd') { - throw new Meteor.Error('error-room-not-found', `No direct message room found by the id of: ${roomId}`); - } - - const { offset, count } = await getPaginationItems(this.queryParams); - const { sort, fields, query } = await this.parseJsonQuery(); - const ourQuery = Object.assign({}, query, { rid: room._id }); - - const { cursor, totalCount } = Messages.findPaginated(ourQuery, { - sort: sort || { ts: -1 }, - skip: offset, - limit: count, - projection: fields, - }); - - const [msgs, total] = await Promise.all([cursor.toArray(), totalCount]); - - if (!msgs) { - throw new Meteor.Error('error-no-messages', 'No messages found'); - } - - return API.v1.success({ - messages: await normalizeMessagesForUser(msgs, this.userId), - offset, - count: msgs.length, - total, - }); - }, - }, -); - -API.v1.addRoute( - ['dm.list', 'im.list'], - { authRequired: true }, - { - async get() { - const { offset, count } = await getPaginationItems(this.queryParams); - const { sort = { name: 1 }, fields } = await this.parseJsonQuery(); - - // TODO: CACHE: Add Breaking notice since we removed the query param - - const subscriptions = await Subscriptions.find({ 'u._id': this.userId, 't': 'd' }, { projection: { rid: 1 } }) - .map((item) => item.rid) - .toArray(); - - const { cursor, totalCount } = Rooms.findPaginated( - { t: 'd', _id: { $in: subscriptions } }, - { - sort, - skip: offset, - limit: count, - projection: fields, - }, - ); - - const [ims, total] = await Promise.all([cursor.toArray(), totalCount]); - - return API.v1.success({ - ims: await Promise.all(ims.map((room: IRoom) => composeRoomWithLastMessage(room, this.userId))), - offset, - count: ims.length, - total, - }); - }, - }, -); - -API.v1.addRoute( - ['dm.list.everyone', 'im.list.everyone'], - { authRequired: true, permissionsRequired: ['view-room-administration'] }, - { - async get() { - const { offset, count }: { offset: number; count: number } = await getPaginationItems(this.queryParams); - const { sort, fields, query } = await this.parseJsonQuery(); - - const { cursor, totalCount } = Rooms.findPaginated( - { ...query, t: 'd' }, - { - sort: sort || { name: 1 }, - skip: offset, - limit: count, - projection: fields, - }, - ); - - const [rooms, total] = await Promise.all([cursor.toArray(), totalCount]); - - return API.v1.success({ - ims: await Promise.all(rooms.map((room: IRoom) => composeRoomWithLastMessage(room, this.userId))), - offset, - count: rooms.length, - total, - }); - }, - }, -); + .get('im.history', dmHistoryEndpointsProps, dmHistoryAction('im.history')) + .get('dm.messages.others', dmMessagesOthersEndpointsProps, dmMessagesOthersAction('dm.messages.others')) + .get('im.messages.others', dmMessagesOthersEndpointsProps, dmMessagesOthersAction('im.messages.others')) + .get('dm.list', dmListEndpointsProps, dmListAction('dm.list')) + .get('im.list', dmListEndpointsProps, dmListAction('im.list')) + .get('dm.list.everyone', dmListEveryoneEndpointsProps, dmListEveryoneAction('dm.list.everyone')) + .get('im.list.everyone', dmListEveryoneEndpointsProps, dmListEveryoneAction('im.list.everyone')); export type DmEndpoints = ExtractRoutesFromAPI; From 693c99d558cf78072ff6d7f5aa142b0a102f0356 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Mon, 23 Mar 2026 16:29:04 -0300 Subject: [PATCH 19/25] fix(tests): update errorType assertions for migrated chat endpoints The typed endpoint pattern returns 'error-invalid-params' instead of 'invalid-params' when query/body validation fails. Co-Authored-By: Claude Opus 4.6 (1M context) --- apps/meteor/tests/end-to-end/api/chat.ts | 34 ++++++++++++------------ 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/apps/meteor/tests/end-to-end/api/chat.ts b/apps/meteor/tests/end-to-end/api/chat.ts index 7fd725ed5bfbc..056c7194710c8 100644 --- a/apps/meteor/tests/end-to-end/api/chat.ts +++ b/apps/meteor/tests/end-to-end/api/chat.ts @@ -1922,7 +1922,7 @@ describe('[Chat]', () => { .expect(400) .expect((res) => { expect(res.body).to.have.property('success', false); - expect(res.body).to.have.property('errorType', 'invalid-params'); + expect(res.body).to.have.property('errorType', 'error-invalid-params'); }); }); @@ -1938,7 +1938,7 @@ describe('[Chat]', () => { .expect(400) .expect((res) => { expect(res.body).to.have.property('success', false); - expect(res.body).to.have.property('errorType', 'invalid-params'); + expect(res.body).to.have.property('errorType', 'error-invalid-params'); }); }); @@ -1954,7 +1954,7 @@ describe('[Chat]', () => { .expect(400) .expect((res) => { expect(res.body).to.have.property('success', false); - expect(res.body).to.have.property('errorType', 'invalid-params'); + expect(res.body).to.have.property('errorType', 'error-invalid-params'); }); }); @@ -2241,7 +2241,7 @@ describe('[Chat]', () => { .expect(400) .expect((res) => { expect(res.body).to.have.property('success', false); - expect(res.body).to.have.property('errorType', 'invalid-params'); + expect(res.body).to.have.property('errorType', 'error-invalid-params'); }); }); it('should fail deleting a message if no room id is provided', async () => { @@ -2255,7 +2255,7 @@ describe('[Chat]', () => { .expect(400) .expect((res) => { expect(res.body).to.have.property('success', false); - expect(res.body).to.have.property('errorType', 'invalid-params'); + expect(res.body).to.have.property('errorType', 'error-invalid-params'); }); }); it('should fail deleting a message if it is not in the provided room', async () => { @@ -3416,7 +3416,7 @@ describe('[Chat]', () => { .expect(400) .expect((res) => { expect(res.body).to.have.property('success', false); - expect(res.body.errorType).to.be.equal('invalid-params'); + expect(res.body.errorType).to.be.equal('error-invalid-params'); }) .end(done); }); @@ -3475,7 +3475,7 @@ describe('[Chat]', () => { .expect(400) .expect((res) => { expect(res.body).to.have.property('success', false); - expect(res.body.errorType).to.be.equal('invalid-params'); + expect(res.body.errorType).to.be.equal('error-invalid-params'); }) .end(done); }); @@ -3676,7 +3676,7 @@ describe('[Chat]', () => { .expect(400) .expect((res) => { expect(res.body).to.have.property('success', false); - expect(res.body.errorType).to.be.equal('invalid-params'); + expect(res.body.errorType).to.be.equal('error-invalid-params'); expect(res.body.error).to.include(`must have required property 'roomId'`); }) .end(done); @@ -3736,7 +3736,7 @@ describe('[Chat]', () => { .expect(400) .expect((res) => { expect(res.body).to.have.property('success', false); - expect(res.body.errorType).to.be.equal('invalid-params'); + expect(res.body.errorType).to.be.equal('error-invalid-params'); expect(res.body.error).to.include('must be equal to one of the allowed values'); }) .end(done); @@ -4111,7 +4111,7 @@ describe('Threads', () => { .expect(400) .expect((res) => { expect(res.body).to.have.property('success', false); - expect(res.body).to.have.property('errorType', 'invalid-params'); + expect(res.body).to.have.property('errorType', 'error-invalid-params'); }); }); @@ -4127,7 +4127,7 @@ describe('Threads', () => { .expect(400) .expect((res) => { expect(res.body).to.have.property('success', false); - expect(res.body).to.have.property('errorType', 'invalid-params'); + expect(res.body).to.have.property('errorType', 'error-invalid-params'); }); }); @@ -4310,7 +4310,7 @@ describe('Threads', () => { .expect(400) .expect((res) => { expect(res.body).to.have.property('success', false); - expect(res.body).to.have.property('errorType', 'invalid-params'); + expect(res.body).to.have.property('errorType', 'error-invalid-params'); }) .end(done); }); @@ -4328,7 +4328,7 @@ describe('Threads', () => { .expect(400) .expect((res) => { expect(res.body).to.have.property('success', false); - expect(res.body).to.have.property('errorType', 'invalid-params'); + expect(res.body).to.have.property('errorType', 'error-invalid-params'); }) .end(done); }); @@ -4347,7 +4347,7 @@ describe('Threads', () => { .expect(400) .expect((res) => { expect(res.body).to.have.property('success', false); - expect(res.body).to.have.property('errorType', 'invalid-params'); + expect(res.body).to.have.property('errorType', 'error-invalid-params'); }) .end(done); }); @@ -4568,7 +4568,7 @@ describe('Threads', () => { .expect(400) .expect((res) => { expect(res.body).to.have.property('success', false); - expect(res.body).to.have.property('errorType', 'invalid-params'); + expect(res.body).to.have.property('errorType', 'error-invalid-params'); }) .end(done); }); @@ -4586,7 +4586,7 @@ describe('Threads', () => { .expect(400) .expect((res) => { expect(res.body).to.have.property('success', false); - expect(res.body).to.have.property('errorType', 'invalid-params'); + expect(res.body).to.have.property('errorType', 'error-invalid-params'); }) .end(done); }); @@ -4605,7 +4605,7 @@ describe('Threads', () => { .expect(400) .expect((res) => { expect(res.body).to.have.property('success', false); - expect(res.body).to.have.property('errorType', 'invalid-params'); + expect(res.body).to.have.property('errorType', 'error-invalid-params'); }) .end(done); }); From 8cf1d86807a14c0c849547ae5137fe9414cefc4e Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Mon, 23 Mar 2026 19:23:43 -0300 Subject: [PATCH 20/25] fix(tests): revert errorType for POST endpoints with body validators POST endpoints using body validators return 'invalid-params' (not 'error-invalid-params'). Only GET endpoints with query validators use the 'error-' prefix. Co-Authored-By: Claude Opus 4.6 (1M context) --- apps/meteor/tests/end-to-end/api/chat.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/meteor/tests/end-to-end/api/chat.ts b/apps/meteor/tests/end-to-end/api/chat.ts index 056c7194710c8..2b0cd868effb3 100644 --- a/apps/meteor/tests/end-to-end/api/chat.ts +++ b/apps/meteor/tests/end-to-end/api/chat.ts @@ -1922,7 +1922,7 @@ describe('[Chat]', () => { .expect(400) .expect((res) => { expect(res.body).to.have.property('success', false); - expect(res.body).to.have.property('errorType', 'error-invalid-params'); + expect(res.body).to.have.property('errorType', 'invalid-params'); }); }); @@ -1938,7 +1938,7 @@ describe('[Chat]', () => { .expect(400) .expect((res) => { expect(res.body).to.have.property('success', false); - expect(res.body).to.have.property('errorType', 'error-invalid-params'); + expect(res.body).to.have.property('errorType', 'invalid-params'); }); }); @@ -1954,7 +1954,7 @@ describe('[Chat]', () => { .expect(400) .expect((res) => { expect(res.body).to.have.property('success', false); - expect(res.body).to.have.property('errorType', 'error-invalid-params'); + expect(res.body).to.have.property('errorType', 'invalid-params'); }); }); @@ -2241,7 +2241,7 @@ describe('[Chat]', () => { .expect(400) .expect((res) => { expect(res.body).to.have.property('success', false); - expect(res.body).to.have.property('errorType', 'error-invalid-params'); + expect(res.body).to.have.property('errorType', 'invalid-params'); }); }); it('should fail deleting a message if no room id is provided', async () => { @@ -2255,7 +2255,7 @@ describe('[Chat]', () => { .expect(400) .expect((res) => { expect(res.body).to.have.property('success', false); - expect(res.body).to.have.property('errorType', 'error-invalid-params'); + expect(res.body).to.have.property('errorType', 'invalid-params'); }); }); it('should fail deleting a message if it is not in the provided room', async () => { From 74eb9251a0bf7bd68b6d0e9fc3896f1b19de1216 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Tue, 24 Mar 2026 00:00:47 -0300 Subject: [PATCH 21/25] fix(core-typings): make author_link optional in MessageQuoteAttachment and prevent null md createQuoteAttachment was propagating null/undefined md from messages. Now only spreads md when truthy. Also author_link was never set in createQuoteAttachment so it must be optional in the type. Co-Authored-By: Claude Opus 4.6 (1M context) --- apps/meteor/lib/createQuoteAttachment.ts | 2 +- .../src/IMessage/MessageAttachment/MessageQuoteAttachment.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/meteor/lib/createQuoteAttachment.ts b/apps/meteor/lib/createQuoteAttachment.ts index d7a757b1549d9..7f2ca4e62202a 100644 --- a/apps/meteor/lib/createQuoteAttachment.ts +++ b/apps/meteor/lib/createQuoteAttachment.ts @@ -9,7 +9,7 @@ export function createQuoteAttachment( ) { return { text: message.msg, - md: message.md, + ...(message.md && { md: message.md }), ...(isTranslatedMessage(message) && { translations: message?.translations }), message_link: messageLink, author_name: message.alias || getUserDisplayName(message.u.name, message.u.username, useRealName), diff --git a/packages/core-typings/src/IMessage/MessageAttachment/MessageQuoteAttachment.ts b/packages/core-typings/src/IMessage/MessageAttachment/MessageQuoteAttachment.ts index 237e8db4abb7a..601a72dbee484 100644 --- a/packages/core-typings/src/IMessage/MessageAttachment/MessageQuoteAttachment.ts +++ b/packages/core-typings/src/IMessage/MessageAttachment/MessageQuoteAttachment.ts @@ -5,7 +5,7 @@ import type { MessageAttachmentBase } from './MessageAttachmentBase'; export type MessageQuoteAttachment = { author_name: string; - author_link: string; + author_link?: string; author_icon: string; message_link?: string; text: string; From a1e52c9c9bfc6124b6cf8c4f162c174cac69cf01 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Tue, 24 Mar 2026 00:58:23 -0300 Subject: [PATCH 22/25] fix(core-typings): fix MessageAttachment oneOf discrimination for quote attachments - Make message_link required in MessageQuoteAttachment (serves as discriminator for AJV oneOf) - Patch MessageAttachmentDefault schema at runtime to add additionalProperties: false, preventing it from matching every attachment type in the oneOf Co-Authored-By: Claude Opus 4.6 (1M context) --- apps/meteor/app/api/server/ajv.ts | 7 +++++++ .../IMessage/MessageAttachment/MessageQuoteAttachment.ts | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/apps/meteor/app/api/server/ajv.ts b/apps/meteor/app/api/server/ajv.ts index 00944159988ee..10556de26582f 100644 --- a/apps/meteor/app/api/server/ajv.ts +++ b/apps/meteor/app/api/server/ajv.ts @@ -3,6 +3,13 @@ import { ajv, ajvQuery } from '@rocket.chat/rest-typings'; const components = schemas.components?.schemas; if (components) { + // Patch MessageAttachmentDefault to reject unknown properties so the oneOf + // discriminator works correctly (otherwise it matches every attachment). + const mad = components.MessageAttachmentDefault; + if (mad && typeof mad === 'object' && 'type' in mad) { + (mad as Record).additionalProperties = false; + } + for (const key in components) { if (Object.prototype.hasOwnProperty.call(components, key)) { const uri = `#/components/schemas/${key}`; diff --git a/packages/core-typings/src/IMessage/MessageAttachment/MessageQuoteAttachment.ts b/packages/core-typings/src/IMessage/MessageAttachment/MessageQuoteAttachment.ts index 601a72dbee484..03c1fcb9cb5e5 100644 --- a/packages/core-typings/src/IMessage/MessageAttachment/MessageQuoteAttachment.ts +++ b/packages/core-typings/src/IMessage/MessageAttachment/MessageQuoteAttachment.ts @@ -7,7 +7,7 @@ export type MessageQuoteAttachment = { author_name: string; author_link?: string; author_icon: string; - message_link?: string; + message_link: string; text: string; md?: Root; attachments?: Array; // TODO this is causing issues to define a model, see @ts-expect-error at apps/meteor/app/api/server/v1/channels.ts:274 From 86376087074f21b0c9d9dac0a9bfaee19c1889a9 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Tue, 24 Mar 2026 02:11:55 -0300 Subject: [PATCH 23/25] fix(core-typings): add attachments field to MessageAttachmentDefault The pin message attachment includes nested attachments but the type did not declare it, causing AJV validation to reject it with additionalProperties: false. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../src/IMessage/MessageAttachment/MessageAttachmentDefault.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/core-typings/src/IMessage/MessageAttachment/MessageAttachmentDefault.ts b/packages/core-typings/src/IMessage/MessageAttachment/MessageAttachmentDefault.ts index 576e92985ba96..3cce1a67f241a 100644 --- a/packages/core-typings/src/IMessage/MessageAttachment/MessageAttachmentDefault.ts +++ b/packages/core-typings/src/IMessage/MessageAttachment/MessageAttachmentDefault.ts @@ -1,5 +1,6 @@ import type { Root } from '@rocket.chat/message-parser'; +import type { MessageAttachment } from './MessageAttachment'; import type { MessageAttachmentBase } from './MessageAttachmentBase'; export type MarkdownFields = 'text' | 'pretext' | 'fields'; @@ -32,4 +33,6 @@ export type MessageAttachmentDefault = { thumb_url?: string; color?: string; + + attachments?: MessageAttachment[]; } & MessageAttachmentBase; From 568c879d1fc6d9e3bfcdf0f1b1b679f7cc95b02f Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Tue, 24 Mar 2026 02:15:33 -0300 Subject: [PATCH 24/25] fix(core-typings): add content field to MessageAttachmentDefault Pin attachments include encrypted content from e2e messages. Without this field, additionalProperties: false rejects the attachment. Co-Authored-By: Claude Opus 4.6 (1M context) --- apps/meteor/app/message-pin/server/pinMessage.ts | 4 ++-- .../IMessage/MessageAttachment/MessageAttachmentDefault.ts | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/apps/meteor/app/message-pin/server/pinMessage.ts b/apps/meteor/app/message-pin/server/pinMessage.ts index 204868d9dcda3..14de6d696f9b4 100644 --- a/apps/meteor/app/message-pin/server/pinMessage.ts +++ b/apps/meteor/app/message-pin/server/pinMessage.ts @@ -119,7 +119,7 @@ export async function pinMessage(message: IMessage, userId: string, pinnedAt?: D text: originalMessage.msg, author_name: originalMessage.u.username, author_icon: getUserAvatarURL(originalMessage.u.username), - content: originalMessage.content, + ...(originalMessage.content && { content: originalMessage.content }), ts: originalMessage.ts, attachments: attachments.map(recursiveRemove), }, @@ -136,7 +136,7 @@ export const unpinMessage = async (userId: string, message: IMessage) => { } let originalMessage = await Messages.findOneById(message._id); - if (originalMessage == null || originalMessage._id == null) { + if (originalMessage?._id == null) { throw new Meteor.Error('error-invalid-message', 'Message you are unpinning was not found', { method: 'unpinMessage', action: 'Message_pinning', diff --git a/packages/core-typings/src/IMessage/MessageAttachment/MessageAttachmentDefault.ts b/packages/core-typings/src/IMessage/MessageAttachment/MessageAttachmentDefault.ts index 3cce1a67f241a..9a3b6f4c123be 100644 --- a/packages/core-typings/src/IMessage/MessageAttachment/MessageAttachmentDefault.ts +++ b/packages/core-typings/src/IMessage/MessageAttachment/MessageAttachmentDefault.ts @@ -35,4 +35,7 @@ export type MessageAttachmentDefault = { color?: string; attachments?: MessageAttachment[]; + + /** Encrypted content from e2e messages, preserved in pin attachments */ + content?: object; } & MessageAttachmentBase; From 5d9b1db45283063a0a3875bb1741b55b6b4b5bea Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Tue, 24 Mar 2026 02:54:41 -0300 Subject: [PATCH 25/25] fix(autotranslate): remove unnecessary TypeScript error suppression for attachment translation Eliminated the @ts-expect-error comments in the AutoTranslate module, clarifying the handling of attachment translations. Updated MessageAttachmentDefault type with a TODO comment to verify the validity of the content field usage. --- apps/meteor/app/autotranslate/client/lib/autotranslate.ts | 1 - .../src/IMessage/MessageAttachment/MessageAttachmentDefault.ts | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/apps/meteor/app/autotranslate/client/lib/autotranslate.ts b/apps/meteor/app/autotranslate/client/lib/autotranslate.ts index 76b45d04e18fa..b1243cbe892ac 100644 --- a/apps/meteor/app/autotranslate/client/lib/autotranslate.ts +++ b/apps/meteor/app/autotranslate/client/lib/autotranslate.ts @@ -80,7 +80,6 @@ export const AutoTranslate = { } } - // @ts-expect-error - not sure what to do with this if (attachment.attachments && attachment.attachments.length > 0) { // @ts-expect-error - not sure what to do with this attachment.attachments = this.translateAttachments(attachment.attachments, language); diff --git a/packages/core-typings/src/IMessage/MessageAttachment/MessageAttachmentDefault.ts b/packages/core-typings/src/IMessage/MessageAttachment/MessageAttachmentDefault.ts index 9a3b6f4c123be..395c5f437962c 100644 --- a/packages/core-typings/src/IMessage/MessageAttachment/MessageAttachmentDefault.ts +++ b/packages/core-typings/src/IMessage/MessageAttachment/MessageAttachmentDefault.ts @@ -37,5 +37,5 @@ export type MessageAttachmentDefault = { attachments?: MessageAttachment[]; /** Encrypted content from e2e messages, preserved in pin attachments */ - content?: object; + content?: object; // TODO: check if MessageAttachmentDefault[content] is a valid type it does not seem to be used anywhere } & MessageAttachmentBase;