Skip to content
Closed
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
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
167 changes: 126 additions & 41 deletions apps/meteor/app/api/server/v1/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,12 @@ import {
isChatGetDeletedMessagesProps,
isChatSyncThreadsListProps,
isChatGetThreadMessagesProps,
isChatSyncThreadMessagesProps,
isChatGetStarredMessagesProps,
isChatGetDiscussionsProps,
validateBadRequestErrorResponse,
validateUnauthorizedErrorResponse,
} from '@rocket.chat/rest-typings';
import type { PaginatedRequest } from '@rocket.chat/rest-typings';
import { escapeRegExp } from '@rocket.chat/string-helpers';
import { Meteor } from 'meteor/meteor';

Expand All @@ -50,8 +50,8 @@ import { settings } from '../../../settings/server';
import { followMessage } from '../../../threads/server/methods/followMessage';
import { unfollowMessage } from '../../../threads/server/methods/unfollowMessage';
import { normalizeMessagesForUser } from '../../../utils/server/lib/normalizeMessagesForUser';
import type { ExtractRoutesFromAPI } from '../ApiClass';
import { API } from '../api';
import type { ExtractRoutesFromAPI } from '../ApiClass';
import { getPaginationItems } from '../helpers/getPaginationItems';
import { findDiscussionsFromRoom, findMentionedMessages, findStarredMessages } from '../lib/messages';

Expand Down Expand Up @@ -275,6 +275,85 @@ const isChatPinMessageProps = ajv.compile<ChatPinMessage>(ChatPinMessageSchema);

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

type ChatSyncThreadMessages = PaginatedRequest<{
tmid: string;
updatedSince: string;
}>;

const ChatSyncThreadMessagesSchema = {
type: 'object',
properties: {
tmid: {
type: 'string',
minLength: 1,
},
updatedSince: {
type: 'string',
format: 'iso-date-time',
},
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', 'updatedSince'],
additionalProperties: false,
};

const isChatSyncThreadMessagesLocalProps = ajv.compile<ChatSyncThreadMessages>(ChatSyncThreadMessagesSchema);
Copy link
Copy Markdown
Contributor

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

Choose a reason for hiding this comment

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

P2: Query validator for chat.syncThreadMessages is compiled with non-coercing AJV while schema expects numeric query params, risking 400s for normal string URL values (count, offset).

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 319:

<comment>Query validator for `chat.syncThreadMessages` is compiled with non-coercing AJV while schema expects numeric query params, risking 400s for normal string URL values (`count`, `offset`).</comment>

<file context>
@@ -275,6 +275,85 @@ const isChatPinMessageProps = ajv.compile<ChatPinMessage>(ChatPinMessageSchema);
+	additionalProperties: false,
+};
+
+const isChatSyncThreadMessagesLocalProps = ajv.compile<ChatSyncThreadMessages>(ChatSyncThreadMessagesSchema);
+
+const isSyncThreadMessagesResponse = ajv.compile<{
</file context>
Fix with Cubic


const isSyncThreadMessagesResponse = ajv.compile<{
messages: {
update: IMessage[];
remove: IMessage[];
};
}>({
type: 'object',
properties: {
messages: {
type: 'object',
properties: {
update: {
type: 'array',
items: {
$ref: '#/components/schemas/IMessage',
},
},
remove: {
type: 'array',
items: {
$ref: '#/components/schemas/IMessage',
},
},
},
required: ['update', 'remove'],
additionalProperties: false,
},
success: {
type: 'boolean',
enum: [true],
},
},
required: ['messages', 'success'],
additionalProperties: false,
});

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

return API.v1.success();
},
)
.get(
'chat.syncThreadMessages',
{
authRequired: true,
query: isChatSyncThreadMessagesLocalProps,
response: {
400: validateBadRequestErrorResponse,
401: validateUnauthorizedErrorResponse,
200: isSyncThreadMessagesResponse,
},
},
async function action() {
const { tmid } = this.queryParams;
const { query, fields, sort } = await this.parseJsonQuery();
const { updatedSince } = this.queryParams;
let updatedSinceDate;
if (!settings.get<boolean>('Threads_enabled')) {
throw new Meteor.Error('error-not-allowed', 'Threads Disabled');
}

if (isNaN(Date.parse(updatedSince))) {
throw new Meteor.Error('error-updatedSince-param-invalid', 'The "updatedSince" query parameter must be a valid date.');
} else {
updatedSinceDate = new Date(updatedSince);
}
const thread = await Messages.findOneById(tmid, { projection: { rid: 1 } });
if (!thread?.rid) {
throw new Meteor.Error('error-invalid-message', 'Invalid Message');
}
const [user, room] = await Promise.all([
Users.findOneById(this.userId, { projection: { _id: 1 } }),
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');
}
return API.v1.success({
messages: {
update: await Messages.find({ ...query, tmid, _updatedAt: { $gt: updatedSinceDate } }, { projection: fields, sort }).toArray(),
remove: await Messages.trashFindDeletedAfter(updatedSinceDate, { ...query, tmid }, { projection: fields, sort }).toArray(),
},
});
},
);

API.v1.addRoute(
Expand Down Expand Up @@ -918,45 +1042,6 @@ API.v1.addRoute(
},
);

API.v1.addRoute(
'chat.syncThreadMessages',
{ authRequired: true, validateParams: isChatSyncThreadMessagesProps },
{
async get() {
const { tmid } = this.queryParams;
const { query, fields, sort } = await this.parseJsonQuery();
const { updatedSince } = this.queryParams;
let updatedSinceDate;
if (!settings.get<boolean>('Threads_enabled')) {
throw new Meteor.Error('error-not-allowed', 'Threads Disabled');
}

if (isNaN(Date.parse(updatedSince))) {
throw new Meteor.Error('error-updatedSince-param-invalid', 'The "updatedSince" query parameter must be a valid date.');
} else {
updatedSinceDate = new Date(updatedSince);
}
const thread = await Messages.findOneById(tmid, { projection: { rid: 1 } });
if (!thread?.rid) {
throw new Meteor.Error('error-invalid-message', 'Invalid Message');
}
// TODO: promise.all? this.user?
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');
}
return API.v1.success({
messages: {
update: await Messages.find({ ...query, tmid, _updatedAt: { $gt: updatedSinceDate } }, { projection: fields, sort }).toArray(),
remove: await Messages.trashFindDeletedAfter(updatedSinceDate, { ...query, tmid }, { projection: fields, sort }).toArray(),
},
});
},
},
);

API.v1.addRoute(
'chat.getMentionedMessages',
{ authRequired: true, validateParams: isChatGetMentionedMessagesProps },
Expand Down
6 changes: 3 additions & 3 deletions apps/meteor/tests/end-to-end/api/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
Expand All @@ -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);
});
Expand All @@ -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);
});
Expand Down
43 changes: 0 additions & 43 deletions packages/rest-typings/src/v1/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -638,41 +638,6 @@ const ChatSyncMessagesSchema = {

export const isChatSyncMessagesProps = ajvQuery.compile<ChatSyncMessages>(ChatSyncMessagesSchema);

type ChatSyncThreadMessages = PaginatedRequest<{
tmid: string;
updatedSince: string;
}>;

const ChatSyncThreadMessagesSchema = {
type: 'object',
properties: {
tmid: {
type: 'string',
minLength: 1,
},
updatedSince: {
type: 'string',
format: 'iso-date-time',
},
count: {
type: 'number',
nullable: true,
},
offset: {
type: 'number',
nullable: true,
},
sort: {
type: 'string',
nullable: true,
},
},
required: ['tmid', 'updatedSince'],
additionalProperties: false,
};

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

type ChatGetThreadMessages = PaginatedRequest<{
tmid: string;
}>;
Expand Down Expand Up @@ -989,14 +954,6 @@ export type ChatEndpoints = {
message: IMessage;
};
};
'/v1/chat.syncThreadMessages': {
GET: (params: ChatSyncThreadMessages) => {
messages: {
update: IMessage[];
remove: IMessage[];
};
};
};
'/v1/chat.getThreadMessages': {
GET: (params: ChatGetThreadMessages) => {
messages: IMessage[];
Expand Down