Skip to content
Closed
Show file tree
Hide file tree
Changes from 5 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
191 changes: 139 additions & 52 deletions apps/meteor/app/api/server/v1/chat.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { Message } from '@rocket.chat/core-services';
import type { IMessage, IThreadMainMessage } from '@rocket.chat/core-typings';
import type { IMessage, IThreadMainMessage, IRoom } from '@rocket.chat/core-typings';
import { MessageTypes } from '@rocket.chat/message-types';
import { Messages, Users, Rooms, Subscriptions } from '@rocket.chat/models';
import type { PaginatedRequest } from '@rocket.chat/rest-typings';
import {
ajv,
isChatReportMessageProps,
isChatGetURLPreviewProps,
isChatUpdateProps,
isChatGetThreadsListProps,
isChatDeleteProps,
isChatSyncMessagesProps,
isChatGetMessageProps,
Expand Down Expand Up @@ -275,6 +275,88 @@ const isChatPinMessageProps = ajv.compile<ChatPinMessage>(ChatPinMessageSchema);

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

type ChatGetThreadsList = PaginatedRequest<{
rid: IRoom['_id'];
type?: 'unread' | 'following';
text?: string;
fields?: string;
}>;

const ChatGetThreadsListSchema = {
type: 'object',
properties: {
rid: {
type: 'string',
},
type: {
type: 'string',
enum: ['following', 'unread'],
nullable: true,
},
text: {
type: 'string',
nullable: true,
},
offset: {
type: 'number',
nullable: true,
},
count: {
type: 'number',
nullable: true,
},
sort: {
type: 'string',
nullable: true,
},
query: {
type: 'string',
nullable: true,
},
fields: {
type: 'string',
nullable: true,
},
},
required: ['rid'],
additionalProperties: false,
};

const isChatGetThreadsListLocalProps = ajv.compile<ChatGetThreadsList>(ChatGetThreadsListSchema);

const isChatGetThreadsListResponse = ajv.compile<{
threads: IMessage[];
count: number;
offset: number;
total: number;
success: boolean;
}>({
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,
});
Comment on lines +278 to +358
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Critical migration gap: chat.syncThreadMessages still depends on a removed validator export.

After moving chat.getThreadsList to local typed validation here, chat.syncThreadMessages (Line 1010) still uses isChatSyncThreadMessagesProps from @rocket.chat/rest-typings. That export was removed in this PR scope, which can break import/type resolution for this file.

Please either (a) keep the rest-typings export until chat.syncThreadMessages is migrated, or (b) migrate chat.syncThreadMessages to a local validator in this file in the same PR.

Suggested patch (option b: localize syncThreadMessages validator)
@@
 import {
@@
-	isChatSyncThreadMessagesProps,
@@
 } from '@rocket.chat/rest-typings';
@@
+type ChatSyncThreadMessagesLocal = PaginatedRequest<{
+	tmid: IMessage['_id'];
+	updatedSince: string;
+}>;
+
+const ChatSyncThreadMessagesLocalSchema = {
+	type: 'object',
+	properties: {
+		tmid: {
+			type: 'string',
+			minLength: 1,
+		},
+		updatedSince: {
+			type: 'string',
+			format: 'iso-date-time',
+		},
+	},
+	required: ['tmid', 'updatedSince'],
+	additionalProperties: false,
+};
+
+const isChatSyncThreadMessagesLocalProps = ajv.compile<ChatSyncThreadMessagesLocal>(ChatSyncThreadMessagesLocalSchema);
@@
 API.v1.addRoute(
 	'chat.syncThreadMessages',
-	{ authRequired: true, validateParams: isChatSyncThreadMessagesProps },
+	{ authRequired: true, validateParams: isChatSyncThreadMessagesLocalProps },
 	{
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/meteor/app/api/server/v1/chat.ts` around lines 278 - 358,
chat.syncThreadMessages still imports and relies on the removed
isChatSyncThreadMessagesProps from `@rocket.chat/rest-typings`; create a local AJV
validator in apps/meteor/app/api/server/v1/chat.ts (mirroring the pattern used
for isChatGetThreadsListLocalProps and isChatGetThreadsListResponse) that
validates the expected payload for chat.syncThreadMessages, compile it (e.g.,
isChatSyncThreadMessagesLocalProps) and update chat.syncThreadMessages to use
this local validator instead of the removed export; ensure the schema matches
the previous shape used by isChatSyncThreadMessagesProps and remove the
now-broken import.


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

return API.v1.success();
},
)
.get(
'chat.getThreadsList',
{
authRequired: true,
query: isChatGetThreadsListLocalProps,
response: {
400: validateBadRequestErrorResponse,
401: validateUnauthorizedErrorResponse,
200: isChatGetThreadsListResponse,
},
},
async function action() {
const { rid, type, text } = this.queryParams;

const { offset, count } = await getPaginationItems(this.queryParams);
const { sort, fields, query } = await this.parseJsonQuery();

if (!settings.get<boolean>('Threads_enabled')) {
throw new Meteor.Error('error-not-allowed', 'Threads Disabled');
}
const user = await Users.findOneById(this.userId, { projection: { _id: 1 } });
const room = await Rooms.findOneById(rid, { projection: { ...roomAccessAttributes, t: 1, _id: 1 } });

if (!room || !user || !(await canAccessRoomAsync(room, user))) {
throw new Meteor.Error('error-not-allowed', 'Not Allowed');
}

const typeThread = {
_hidden: { $ne: true },
...(type === 'following' && { replies: { $in: [this.userId] } }),
...(type === 'unread' && { _id: { $in: (await Subscriptions.findOneByRoomIdAndUserId(room._id, user._id))?.tunread || [] } }),
msg: new RegExp(escapeRegExp(text || ''), 'i'),
};

const threadQuery = { ...query, ...typeThread, rid: room._id, tcount: { $exists: true } };
const { cursor, totalCount } = await Messages.findPaginated<IThreadMainMessage>(threadQuery, {
sort: sort || { tlm: -1 },
skip: offset,
limit: count,
projection: fields,
});

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

return API.v1.success({
threads: await normalizeMessagesForUser(threads, this.userId),
count: threads.length,
offset,
total,
});
},
);

API.v1.addRoute(
Expand Down Expand Up @@ -782,53 +916,6 @@ API.v1.addRoute(
},
);

API.v1.addRoute(
'chat.getThreadsList',
{ authRequired: true, validateParams: isChatGetThreadsListProps },
{
async get() {
const { rid, type, text } = this.queryParams;

const { offset, count } = await getPaginationItems(this.queryParams);
const { sort, fields, query } = await this.parseJsonQuery();

if (!settings.get<boolean>('Threads_enabled')) {
throw new Meteor.Error('error-not-allowed', 'Threads Disabled');
}
const user = await Users.findOneById(this.userId, { projection: { _id: 1 } });
const room = await Rooms.findOneById(rid, { projection: { ...roomAccessAttributes, t: 1, _id: 1 } });

if (!room || !user || !(await canAccessRoomAsync(room, user))) {
throw new Meteor.Error('error-not-allowed', 'Not Allowed');
}

const typeThread = {
_hidden: { $ne: true },
...(type === 'following' && { replies: { $in: [this.userId] } }),
...(type === 'unread' && { _id: { $in: (await Subscriptions.findOneByRoomIdAndUserId(room._id, user._id))?.tunread || [] } }),
msg: new RegExp(escapeRegExp(text || ''), 'i'),
};

const threadQuery = { ...query, ...typeThread, rid: room._id, tcount: { $exists: true } };
const { cursor, totalCount } = await Messages.findPaginated<IThreadMainMessage>(threadQuery, {
sort: sort || { tlm: -1 },
skip: offset,
limit: count,
projection: fields,
});

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

return API.v1.success({
threads: await normalizeMessagesForUser(threads, this.userId),
count: threads.length,
offset,
total,
});
},
},
);

API.v1.addRoute(
'chat.syncThreadsList',
{ authRequired: true, validateParams: isChatSyncThreadsListProps },
Expand Down Expand Up @@ -1050,9 +1137,9 @@ API.v1.addRoute(
},
);


export type ChatEndpoints = ExtractRoutesFromAPI<typeof chatEndpoints>;

declare module '@rocket.chat/rest-typings' {
// eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/no-empty-interface
interface Endpoints extends ChatEndpoints {}
}
interface Endpoints extends ChatEndpoints {}
}
98 changes: 1 addition & 97 deletions packages/rest-typings/src/v1/chat.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { IMessage, IRoom, MessageAttachment, IReadReceiptWithUser, MessageUrl, IThreadMainMessage } from '@rocket.chat/core-typings';
import type { IMessage, IRoom, MessageAttachment, IReadReceiptWithUser, MessageUrl} from '@rocket.chat/core-typings';

import { ajv } from './Ajv';
import type { PaginatedRequest } from '../helpers/PaginatedRequest';
Expand Down Expand Up @@ -146,54 +146,6 @@ const ChatReportMessageSchema = {

export const isChatReportMessageProps = ajv.compile<ChatReportMessage>(ChatReportMessageSchema);

type ChatGetThreadsList = PaginatedRequest<{
rid: IRoom['_id'];
type?: 'unread' | 'following';
text?: string;
fields?: string;
}>;

const ChatGetThreadsListSchema = {
type: 'object',
properties: {
rid: {
type: 'string',
},
type: {
type: 'string',
enum: ['following', 'unread'],
nullable: true,
},
text: {
type: 'string',
nullable: true,
},
offset: {
type: 'number',
nullable: true,
},
count: {
type: 'number',
nullable: true,
},
sort: {
type: 'string',
nullable: true,
},
query: {
type: 'string',
nullable: true,
},
fields: {
type: 'string',
nullable: true,
},
},
required: ['rid'],
additionalProperties: false,
};

export const isChatGetThreadsListProps = ajv.compile<ChatGetThreadsList>(ChatGetThreadsListSchema);

type ChatSyncThreadsList = {
rid: IRoom['_id'];
Expand Down Expand Up @@ -638,40 +590,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 @@ -906,12 +824,6 @@ export type ChatEndpoints = {
total: number;
};
};
'/v1/chat.getThreadsList': {
GET: (params: ChatGetThreadsList) => {
threads: IThreadMainMessage[];
total: number;
};
};
'/v1/chat.syncThreadsList': {
GET: (params: ChatSyncThreadsList) => {
threads: {
Expand Down Expand Up @@ -989,14 +901,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