Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/migrate-chat-getThreadMessages-to-openapi.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@rocket.chat/meteor': minor
'@rocket.chat/rest-typings': minor
---

Migrated chat.getThreadMessages endpoint to new OpenAPI pattern with AJV validation
155 changes: 109 additions & 46 deletions apps/meteor/app/api/server/v1/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,12 @@ import {
isChatReactProps,
isChatGetDeletedMessagesProps,
isChatSyncThreadsListProps,
isChatGetThreadMessagesProps,
isChatSyncThreadMessagesProps,
isChatGetStarredMessagesProps,
isChatGetDiscussionsProps,
validateBadRequestErrorResponse,
validateUnauthorizedErrorResponse,
type PaginatedRequest,
} from '@rocket.chat/rest-typings';
import { escapeRegExp } from '@rocket.chat/string-helpers';
import { Meteor } from 'meteor/meteor';
Expand Down Expand Up @@ -275,6 +275,44 @@ const isChatPinMessageProps = ajv.compile<ChatPinMessage>(ChatPinMessageSchema);

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

type ChatGetThreadMessages = PaginatedRequest<{
tmid: string;
}>;

const ChatGetThreadMessagesSchema = {
type: 'object',
properties: {
tmid: {
type: 'string',
minLength: 1,
},
count: {
type: 'number',
nullable: true,
},
offset: {
type: 'number',
nullable: true,
},
sort: {
type: 'string',
nullable: true,
},
query: {
type: 'string',
nullable: true,
},
fields: {
type: 'string',
nullable: true,
},
},
required: ['tmid'],
additionalProperties: false,
};

const isChatGetThreadMessagesLocalProps = ajv.compile<ChatGetThreadMessages>(ChatGetThreadMessagesSchema);

const chatEndpoints = API.v1
.post(
'chat.pinMessage',
Expand Down Expand Up @@ -558,6 +596,76 @@ const chatEndpoints = API.v1

return API.v1.success();
},
)
.get(
'chat.getThreadMessages',
{
authRequired: true,
query: isChatGetThreadMessagesLocalProps,
response: {
200: ajv.compile<{
messages: IMessage[];
count: number;
offset: number;
total: number;
success: boolean;
}>({
type: 'object',
properties: {
messages: {
type: 'array',
items: { $ref: '#/components/schemas/IMessage' },
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot Mar 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Response schema now enforces full IMessage objects even though this endpoint supports fields projection and can return partial message documents.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/meteor/app/api/server/v1/chat.ts, line 617:

<comment>Response schema now enforces full `IMessage` objects even though this endpoint supports `fields` projection and can return partial message documents.</comment>

<file context>
@@ -614,7 +612,10 @@ const chatEndpoints = API.v1
-						messages: { type: 'array' },
+						messages: {
+							type: 'array',
+							items: { $ref: '#/components/schemas/IMessage' },
+						},
 						count: { type: 'number' },
</file context>
Suggested change
items: { $ref: '#/components/schemas/IMessage' },
items: { type: 'object' },
Fix with Cubic

},
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);

if (!settings.get('Threads_enabled')) {
throw new Meteor.Error('error-not-allowed', 'Threads Disabled');
}

const thread = await Messages.findOneById(tmid, { projection: { rid: 1 } });
if (!thread?.rid) {
throw new Meteor.Error('error-invalid-message', 'Invalid Message');
}
const user = await Users.findOneById(this.userId, { projection: { _id: 1 } });
const room = await Rooms.findOneById(thread.rid, { projection: { ...roomAccessAttributes, t: 1, _id: 1 } });

if (!room || !user || !(await canAccessRoomAsync(room, user))) {
throw new Meteor.Error('error-not-allowed', 'Not Allowed');
}
const { cursor, totalCount } = Messages.findPaginated(
{ ...query, tmid },
{
sort: sort || { ts: 1 },
skip: offset,
limit: count,
projection: fields,
},
);

const [messages, total] = await Promise.all([cursor.toArray(), totalCount]);

return API.v1.success({
messages,
count: messages.length,
offset,
total,
});
},
);

API.v1.addRoute(
Expand Down Expand Up @@ -873,51 +981,6 @@ API.v1.addRoute(
},
);

API.v1.addRoute(
'chat.getThreadMessages',
{ authRequired: true, validateParams: isChatGetThreadMessagesProps },
{
async get() {
const { tmid } = this.queryParams;
const { query, fields, sort } = await this.parseJsonQuery();
const { offset, count } = await getPaginationItems(this.queryParams);

if (!settings.get('Threads_enabled')) {
throw new Meteor.Error('error-not-allowed', 'Threads Disabled');
}

const thread = await Messages.findOneById(tmid, { projection: { rid: 1 } });
if (!thread?.rid) {
throw new Meteor.Error('error-invalid-message', 'Invalid Message');
}
const user = await Users.findOneById(this.userId, { projection: { _id: 1 } });
const room = await Rooms.findOneById(thread.rid, { projection: { ...roomAccessAttributes, t: 1, _id: 1 } });

if (!room || !user || !(await canAccessRoomAsync(room, user))) {
throw new Meteor.Error('error-not-allowed', 'Not Allowed');
}
const { cursor, totalCount } = Messages.findPaginated(
{ ...query, tmid },
{
sort: sort || { ts: 1 },
skip: offset,
limit: count,
projection: fields,
},
);

const [messages, total] = await Promise.all([cursor.toArray(), totalCount]);

return API.v1.success({
messages,
count: messages.length,
offset,
total,
});
},
},
);

API.v1.addRoute(
'chat.syncThreadMessages',
{ authRequired: true, validateParams: isChatSyncThreadMessagesProps },
Expand Down
38 changes: 0 additions & 38 deletions packages/rest-typings/src/v1/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -673,36 +673,6 @@ const ChatSyncThreadMessagesSchema = {

export const isChatSyncThreadMessagesProps = ajv.compile<ChatSyncThreadMessages>(ChatSyncThreadMessagesSchema);

type ChatGetThreadMessages = PaginatedRequest<{
tmid: string;
}>;

const ChatGetThreadMessagesSchema = {
type: 'object',
properties: {
tmid: {
type: 'string',
minLength: 1,
},
count: {
type: 'number',
nullable: true,
},
offset: {
type: 'number',
nullable: true,
},
sort: {
type: 'string',
nullable: true,
},
},
required: ['tmid'],
additionalProperties: false,
};

export const isChatGetThreadMessagesProps = ajv.compile<ChatGetThreadMessages>(ChatGetThreadMessagesSchema);

type ChatGetDeletedMessages = PaginatedRequest<{
roomId: IRoom['_id'];
since: string;
Expand Down Expand Up @@ -997,14 +967,6 @@ export type ChatEndpoints = {
};
};
};
'/v1/chat.getThreadMessages': {
GET: (params: ChatGetThreadMessages) => {
messages: IMessage[];
count: number;
offset: number;
total: number;
};
};
'/v1/chat.getDeletedMessages': {
GET: (params: ChatGetDeletedMessages) => {
messages: IMessage[];
Expand Down