Skip to content
Closed
Show file tree
Hide file tree
Changes from 8 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
2 changes: 2 additions & 0 deletions apps/meteor/app/api/server/v1/calendar.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/*
import { Calendar } from '@rocket.chat/core-services';
import type { ICalendarEvent } from '@rocket.chat/core-typings';
import {
Expand Down Expand Up @@ -222,3 +223,4 @@ API.v1.post(
return API.v1.success();
},
);
*/
169 changes: 128 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,87 @@ 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: Partial<IMessage>[];
remove: Partial<IMessage>[];
};
}>({
type: 'object',
properties: {
messages: {
type: 'object',
properties: {
update: {
type: 'array',
items: {
type: 'object',
additionalProperties: true,
},
},
remove: {
type: 'array',
items: {
type: 'object',
additionalProperties: true,
},
},
},
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 +639,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 +1044,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
42 changes: 0 additions & 42 deletions packages/rest-typings/src/v1/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -638,40 +638,6 @@ const ChatSyncMessagesSchema = {

export const isChatSyncMessagesProps = ajv.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 = ajv.compile<ChatSyncThreadMessages>(ChatSyncThreadMessagesSchema);

type ChatGetThreadMessages = PaginatedRequest<{
tmid: string;
Expand Down Expand Up @@ -989,14 +955,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