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/apps/meteor/app/api/server/v1/chat.ts b/apps/meteor/app/api/server/v1/chat.ts index 529aaa19a8ac3..2c9f645583670 100644 --- a/apps/meteor/app/api/server/v1/chat.ts +++ b/apps/meteor/app/api/server/v1/chat.ts @@ -127,118 +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 }, - { - 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', - { - 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']; }; @@ -633,13 +521,205 @@ 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 } }); -API.v1.addRoute( - 'chat.postMessage', - { authRequired: true, validateParams: isChatPostMessageProps }, - { - async post() { + 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, + }); + }, + ) + .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, + }, + }); + }, + ) + .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, + }); + }, + ) + .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; @@ -669,14 +749,27 @@ API.v1.addRoute( 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); @@ -701,17 +794,30 @@ 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 -// 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'"); } @@ -725,14 +831,26 @@ API.v1.addRoute( 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; @@ -750,14 +868,30 @@ API.v1.addRoute( 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); @@ -780,14 +914,30 @@ API.v1.addRoute( 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); @@ -809,14 +959,30 @@ API.v1.addRoute( 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); @@ -856,14 +1022,35 @@ API.v1.addRoute( 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; @@ -900,14 +1087,30 @@ API.v1.addRoute( }, }); }, - }, -); - -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); @@ -945,14 +1148,35 @@ API.v1.addRoute( 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; @@ -984,14 +1208,30 @@ API.v1.addRoute( }, }); }, - }, -); - -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); @@ -1008,14 +1248,30 @@ API.v1.addRoute( 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); @@ -1034,14 +1290,28 @@ API.v1.addRoute( 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); @@ -1058,14 +1328,27 @@ API.v1.addRoute( }); 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))) { @@ -1077,8 +1360,7 @@ API.v1.addRoute( return API.v1.success({ urlPreview }); }, - }, -); + ); export type ChatEndpoints = ExtractRoutesFromAPI; 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; 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/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/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/apps/meteor/tests/end-to-end/api/chat.ts b/apps/meteor/tests/end-to-end/api/chat.ts index 7fd725ed5bfbc..2b0cd868effb3 100644 --- a/apps/meteor/tests/end-to-end/api/chat.ts +++ b/apps/meteor/tests/end-to-end/api/chat.ts @@ -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); }); diff --git a/packages/core-typings/src/IMessage/MessageAttachment/MessageAttachmentDefault.ts b/packages/core-typings/src/IMessage/MessageAttachment/MessageAttachmentDefault.ts index 576e92985ba96..395c5f437962c 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,9 @@ export type MessageAttachmentDefault = { thumb_url?: string; color?: string; + + attachments?: MessageAttachment[]; + + /** Encrypted content from e2e messages, preserved in pin attachments */ + content?: object; // TODO: check if MessageAttachmentDefault[content] is a valid type it does not seem to be used anywhere } & MessageAttachmentBase; diff --git a/packages/core-typings/src/IMessage/MessageAttachment/MessageQuoteAttachment.ts b/packages/core-typings/src/IMessage/MessageAttachment/MessageQuoteAttachment.ts index 237e8db4abb7a..03c1fcb9cb5e5 100644 --- a/packages/core-typings/src/IMessage/MessageAttachment/MessageQuoteAttachment.ts +++ b/packages/core-typings/src/IMessage/MessageAttachment/MessageQuoteAttachment.ts @@ -5,9 +5,9 @@ import type { MessageAttachmentBase } from './MessageAttachmentBase'; export type MessageQuoteAttachment = { author_name: string; - author_link: 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