From 0edb79acac0a8a10daa600d54c19d7a0f24d0fc1 Mon Sep 17 00:00:00 2001 From: Marcos Spessatto Defendi Date: Fri, 17 Mar 2023 19:27:32 -0300 Subject: [PATCH] refactor: Convert chat endpoints to TS (#28417) --- .../app/api/server/v1/{chat.js => chat.ts} | 294 +++++++------- apps/meteor/tests/end-to-end/api/05-chat.js | 1 + packages/rest-typings/src/index.ts | 1 + packages/rest-typings/src/v1/chat.ts | 377 +++++++++++++++--- 4 files changed, 471 insertions(+), 202 deletions(-) rename apps/meteor/app/api/server/v1/{chat.js => chat.ts} (69%) diff --git a/apps/meteor/app/api/server/v1/chat.js b/apps/meteor/app/api/server/v1/chat.ts similarity index 69% rename from apps/meteor/app/api/server/v1/chat.js rename to apps/meteor/app/api/server/v1/chat.ts index 76f042c8cbc..5363b70d66d 100644 --- a/apps/meteor/app/api/server/v1/chat.js +++ b/apps/meteor/app/api/server/v1/chat.ts @@ -1,26 +1,23 @@ -import { escapeRegExp } from '@rocket.chat/string-helpers'; import { Meteor } from 'meteor/meteor'; import { Match, check } from 'meteor/check'; -import { Messages as MessagesRaw } from '@rocket.chat/models'; +import { Messages, Users, Rooms, Subscriptions } from '@rocket.chat/models'; +import { escapeRegExp } from '@rocket.chat/string-helpers'; +import type { IMessage } from '@rocket.chat/core-typings'; -import { Messages } from '../../../models/server'; import { canAccessRoom, canAccessRoomId, roomAccessAttributes, hasPermission } from '../../../authorization/server'; import { normalizeMessagesForUser } from '../../../utils/server/lib/normalizeMessagesForUser'; -import { processWebhookMessage } from '../../../lib/server'; -import { executeSendMessage } from '../../../lib/server/methods/sendMessage'; -import { executeSetReaction } from '../../../reactions/server/setReaction'; import { API } from '../api'; -import Rooms from '../../../models/server/models/Rooms'; -import Users from '../../../models/server/models/Users'; -import Subscriptions from '../../../models/server/models/Subscriptions'; +import { processWebhookMessage } from '../../../lib/server'; import { settings } from '../../../settings/server'; -import { findMentionedMessages, findStarredMessages, findDiscussionsFromRoom } from '../lib/messages'; +import { executeSetReaction } from '../../../reactions/server/setReaction'; +import { findDiscussionsFromRoom, findMentionedMessages, findStarredMessages } from '../lib/messages'; +import { executeSendMessage } from '../../../lib/server/methods/sendMessage'; API.v1.addRoute( 'chat.delete', { authRequired: true }, { - post() { + async post() { check( this.bodyParams, Match.ObjectIncluding({ @@ -30,7 +27,7 @@ API.v1.addRoute( }), ); - const msg = Messages.findOneById(this.bodyParams.msgId, { fields: { u: 1, rid: 1 } }); + 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}".`); @@ -44,13 +41,13 @@ API.v1.addRoute( return API.v1.failure('Unauthorized. You must have the permission "force-delete-message" to delete other\'s message as them.'); } - Meteor.runAsUser(this.bodyParams.asUser ? msg.u._id : this.userId, () => { + await Meteor.runAsUser(this.bodyParams.asUser ? msg.u._id : this.userId, () => { Meteor.call('deleteMessage', { _id: msg._id }); }); return API.v1.success({ _id: msg._id, - ts: Date.now(), + ts: Date.now().toString(), message: msg, }); }, @@ -61,7 +58,7 @@ API.v1.addRoute( 'chat.syncMessages', { authRequired: true }, { - get() { + async get() { const { roomId, lastUpdate } = this.queryParams; if (!roomId) { @@ -74,10 +71,7 @@ API.v1.addRoute( throw new Meteor.Error('error-roomId-param-invalid', 'The "lastUpdate" query parameter must be a valid date.'); } - let result; - Meteor.runAsUser(this.userId, () => { - result = Meteor.call('messages/get', roomId, { lastUpdate: new Date(lastUpdate) }); - }); + const result = await Meteor.call('messages/get', roomId, { lastUpdate: new Date(lastUpdate) }); if (!result) { return API.v1.failure(); @@ -85,7 +79,7 @@ API.v1.addRoute( return API.v1.success({ result: { - updated: normalizeMessagesForUser(result.updated, this.userId), + updated: await normalizeMessagesForUser(result.updated, this.userId), deleted: result.deleted, }, }); @@ -95,23 +89,22 @@ API.v1.addRoute( API.v1.addRoute( 'chat.getMessage', - { authRequired: true }, { - get() { + authRequired: true, + }, + { + async get() { if (!this.queryParams.msgId) { return API.v1.failure('The "msgId" query parameter must be provided.'); } - let msg; - Meteor.runAsUser(this.userId, () => { - msg = Meteor.call('getSingleMessage', this.queryParams.msgId); - }); + const msg = await Meteor.call('getSingleMessage', this.queryParams.msgId); if (!msg) { return API.v1.failure(); } - const [message] = normalizeMessagesForUser([msg], this.userId); + const [message] = await normalizeMessagesForUser([msg], this.userId); return API.v1.success({ message, @@ -124,23 +117,20 @@ API.v1.addRoute( 'chat.pinMessage', { authRequired: true }, { - post() { - if (!this.bodyParams.messageId || !this.bodyParams.messageId.trim()) { + async post() { + if (!this.bodyParams.messageId?.trim()) { throw new Meteor.Error('error-messageid-param-not-provided', 'The required "messageId" param is missing.'); } - const msg = Messages.findOneById(this.bodyParams.messageId); + const msg = await Messages.findOneById(this.bodyParams.messageId); if (!msg) { throw new Meteor.Error('error-message-not-found', 'The provided "messageId" does not match any existing message.'); } - let pinnedMessage; - Meteor.runAsUser(this.userId, () => { - pinnedMessage = Meteor.call('pinMessage', msg); - }); + const pinnedMessage = await Meteor.call('pinMessage', msg); - const [message] = normalizeMessagesForUser([pinnedMessage], this.userId); + const [message] = await normalizeMessagesForUser([pinnedMessage], this.userId); return API.v1.success({ message, @@ -153,14 +143,14 @@ API.v1.addRoute( 'chat.postMessage', { authRequired: true }, { - post() { - const messageReturn = processWebhookMessage(this.bodyParams, this.user)[0]; + async post() { + const messageReturn = await processWebhookMessage(this.bodyParams, this.user)[0]; if (!messageReturn) { return API.v1.failure('unknown-error'); } - const [message] = normalizeMessagesForUser([messageReturn.message], this.userId); + const [message] = await normalizeMessagesForUser([messageReturn.message], this.userId); return API.v1.success({ ts: Date.now(), @@ -175,7 +165,7 @@ API.v1.addRoute( 'chat.search', { authRequired: true }, { - get() { + async get() { const { roomId, searchText } = this.queryParams; const { offset, count } = this.getPaginationItems(); @@ -187,13 +177,10 @@ API.v1.addRoute( throw new Meteor.Error('error-searchText-param-not-provided', 'The required "searchText" query param is missing.'); } - let result; - Meteor.runAsUser(this.userId, () => { - result = Meteor.call('messageSearch', searchText, roomId, count, offset).message.docs; - }); + const result = await Meteor.call('messageSearch', searchText, roomId, count, offset).message.docs; return API.v1.success({ - messages: normalizeMessagesForUser(result, this.userId), + messages: await normalizeMessagesForUser(result, this.userId), }); }, }, @@ -206,13 +193,13 @@ API.v1.addRoute( 'chat.sendMessage', { authRequired: true }, { - post() { + async post() { if (!this.bodyParams.message) { throw new Meteor.Error('error-invalid-params', 'The "message" parameter must be provided.'); } - const sent = executeSendMessage(this.userId, this.bodyParams.message); - const [message] = normalizeMessagesForUser([sent], this.userId); + const sent = await executeSendMessage(this.userId, this.bodyParams.message as Pick); + const [message] = await normalizeMessagesForUser([sent], this.userId); return API.v1.success({ message, @@ -225,24 +212,22 @@ API.v1.addRoute( 'chat.starMessage', { authRequired: true }, { - post() { - if (!this.bodyParams.messageId || !this.bodyParams.messageId.trim()) { + async post() { + if (!this.bodyParams.messageId?.trim()) { throw new Meteor.Error('error-messageid-param-not-provided', 'The required "messageId" param is required.'); } - const msg = Messages.findOneById(this.bodyParams.messageId); + const msg = await Messages.findOneById(this.bodyParams.messageId); if (!msg) { throw new Meteor.Error('error-message-not-found', 'The provided "messageId" does not match any existing message.'); } - Meteor.runAsUser(this.userId, () => - Meteor.call('starMessage', { - _id: msg._id, - rid: msg.rid, - starred: true, - }), - ); + await Meteor.call('starMessage', { + _id: msg._id, + rid: msg.rid, + starred: true, + }); return API.v1.success(); }, @@ -253,18 +238,18 @@ API.v1.addRoute( 'chat.unPinMessage', { authRequired: true }, { - post() { - if (!this.bodyParams.messageId || !this.bodyParams.messageId.trim()) { + async post() { + if (!this.bodyParams.messageId?.trim()) { throw new Meteor.Error('error-messageid-param-not-provided', 'The required "messageId" param is required.'); } - const msg = Messages.findOneById(this.bodyParams.messageId); + const msg = await Messages.findOneById(this.bodyParams.messageId); if (!msg) { throw new Meteor.Error('error-message-not-found', 'The provided "messageId" does not match any existing message.'); } - Meteor.runAsUser(this.userId, () => Meteor.call('unpinMessage', msg)); + await Meteor.call('unpinMessage', msg); return API.v1.success(); }, @@ -275,24 +260,22 @@ API.v1.addRoute( 'chat.unStarMessage', { authRequired: true }, { - post() { - if (!this.bodyParams.messageId || !this.bodyParams.messageId.trim()) { + async post() { + if (!this.bodyParams.messageId?.trim()) { throw new Meteor.Error('error-messageid-param-not-provided', 'The required "messageId" param is required.'); } - const msg = Messages.findOneById(this.bodyParams.messageId); + const msg = await Messages.findOneById(this.bodyParams.messageId); if (!msg) { throw new Meteor.Error('error-message-not-found', 'The provided "messageId" does not match any existing message.'); } - Meteor.runAsUser(this.userId, () => - Meteor.call('starMessage', { - _id: msg._id, - rid: msg.rid, - starred: false, - }), - ); + await Meteor.call('starMessage', { + _id: msg._id, + rid: msg.rid, + starred: false, + }); return API.v1.success(); }, @@ -303,7 +286,7 @@ API.v1.addRoute( 'chat.update', { authRequired: true }, { - post() { + async post() { check( this.bodyParams, Match.ObjectIncluding({ @@ -313,7 +296,7 @@ API.v1.addRoute( }), ); - const msg = Messages.findOneById(this.bodyParams.msgId); + const msg = await Messages.findOneById(this.bodyParams.msgId); // Ensure the message exists if (!msg) { @@ -325,11 +308,10 @@ API.v1.addRoute( } // Permission checks are already done in the updateMessage method, so no need to duplicate them - Meteor.runAsUser(this.userId, () => { - Meteor.call('updateMessage', { _id: msg._id, msg: this.bodyParams.text, rid: msg.rid }); - }); + await Meteor.call('updateMessage', { _id: msg._id, msg: this.bodyParams.text, rid: msg.rid }); - const [message] = normalizeMessagesForUser([Messages.findOneById(msg._id)], this.userId); + const updatedMessage = await Messages.findOneById(msg._id); + const [message] = await normalizeMessagesForUser(updatedMessage ? [updatedMessage] : [], this.userId); return API.v1.success({ message, @@ -342,24 +324,24 @@ API.v1.addRoute( 'chat.react', { authRequired: true }, { - post() { - if (!this.bodyParams.messageId || !this.bodyParams.messageId.trim()) { + async post() { + if (!this.bodyParams.messageId?.trim()) { throw new Meteor.Error('error-messageid-param-not-provided', 'The required "messageId" param is missing.'); } - const msg = Messages.findOneById(this.bodyParams.messageId); + const msg = await Messages.findOneById(this.bodyParams.messageId); if (!msg) { throw new Meteor.Error('error-message-not-found', 'The provided "messageId" does not match any existing message.'); } - const emoji = this.bodyParams.emoji || this.bodyParams.reaction; + const emoji = 'emoji' in this.bodyParams ? this.bodyParams.emoji : (this.bodyParams as { reaction: string }).reaction; if (!emoji) { throw new Meteor.Error('error-emoji-param-not-provided', 'The required "emoji" param is missing.'); } - Meteor.runAsUser(this.userId, () => Promise.await(executeSetReaction(emoji, msg._id, this.bodyParams.shouldReact))); + await executeSetReaction(emoji, msg._id, this.bodyParams.shouldReact); return API.v1.success(); }, @@ -370,7 +352,7 @@ API.v1.addRoute( 'chat.reportMessage', { authRequired: true }, { - post() { + async post() { const { messageId, description } = this.bodyParams; if (!messageId) { return API.v1.failure('The required "messageId" param is missing.'); @@ -380,7 +362,7 @@ API.v1.addRoute( return API.v1.failure('The required "description" param is missing.'); } - Meteor.call('reportMessage', messageId, description); + await Meteor.call('reportMessage', messageId, description); return API.v1.success(); }, @@ -391,21 +373,21 @@ API.v1.addRoute( 'chat.ignoreUser', { authRequired: true }, { - get() { + async get() { const { rid, userId } = this.queryParams; let { ignore = true } = this.queryParams; ignore = typeof ignore === 'string' ? /true|1/.test(ignore) : ignore; - if (!rid || !rid.trim()) { + if (!rid?.trim()) { throw new Meteor.Error('error-room-id-param-not-provided', 'The required "rid" param is missing.'); } - if (!userId || !userId.trim()) { + if (!userId?.trim()) { throw new Meteor.Error('error-user-id-param-not-provided', 'The required "userId" param is missing.'); } - Meteor.runAsUser(this.userId, () => Meteor.call('ignoreUser', { rid, userId, ignore })); + await Meteor.call('ignoreUser', { rid, userId, ignore }); return API.v1.success(); }, @@ -430,7 +412,7 @@ API.v1.addRoute( throw new Meteor.Error('The "since" query parameter must be a valid date.'); } - const { cursor, totalCount } = MessagesRaw.trashFindPaginatedDeletedAfter( + const { cursor, totalCount } = await Messages.trashFindPaginatedDeletedAfter( new Date(since), { rid: roomId }, { @@ -468,7 +450,7 @@ API.v1.addRoute( throw new Meteor.Error('error-not-allowed', 'Not allowed'); } - const { cursor, totalCount } = MessagesRaw.findPaginatedPinnedByRoom(roomId, { + const { cursor, totalCount } = await Messages.findPaginatedPinnedByRoom(roomId, { skip: offset, limit: count, }); @@ -476,7 +458,7 @@ API.v1.addRoute( const [messages, total] = await Promise.all([cursor.toArray(), totalCount]); return API.v1.success({ - messages: normalizeMessagesForUser(messages, this.userId), + messages: await normalizeMessagesForUser(messages, this.userId), count: messages.length, offset, total, @@ -498,25 +480,25 @@ API.v1.addRoute( const { offset, count } = this.getPaginationItems(); const { sort, fields, query } = this.parseJsonQuery(); - if (!settings.get('Threads_enabled')) { + if (!settings.get('Threads_enabled')) { throw new Meteor.Error('error-not-allowed', 'Threads Disabled'); } - const user = Users.findOneById(this.userId, { fields: { _id: 1 } }); - const room = Rooms.findOneById(rid, { fields: { ...roomAccessAttributes, t: 1, _id: 1 } }); + const user = await Users.findOneById(this.userId, { projection: { _id: 1 } }); + const room = await Rooms.findOneById(rid, { projection: { ...roomAccessAttributes, t: 1, _id: 1 } }); - if (!canAccessRoom(room, user)) { + if (!room || !user || !canAccessRoom(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: Subscriptions.findOneByRoomIdAndUserId(room._id, user._id).tunread } }), - msg: new RegExp(escapeRegExp(text), 'i'), + ...(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 } = MessagesRaw.findPaginated(threadQuery, { + const { cursor, totalCount } = await Messages.findPaginated(threadQuery, { sort: sort || { tlm: -1 }, skip: offset, limit: count, @@ -526,7 +508,7 @@ API.v1.addRoute( const [threads, total] = await Promise.all([cursor.toArray(), totalCount]); return API.v1.success({ - threads: normalizeMessagesForUser(threads, this.userId), + threads: await normalizeMessagesForUser(threads, this.userId), count: threads.length, offset, total, @@ -539,12 +521,12 @@ API.v1.addRoute( 'chat.syncThreadsList', { authRequired: true }, { - get() { + async get() { const { rid } = this.queryParams; const { query, fields, sort } = this.parseJsonQuery(); const { updatedSince } = this.queryParams; let updatedSinceDate; - if (!settings.get('Threads_enabled')) { + if (!settings.get('Threads_enabled')) { throw new Meteor.Error('error-not-allowed', 'Threads Disabled'); } if (!rid) { @@ -558,17 +540,26 @@ API.v1.addRoute( } else { updatedSinceDate = new Date(updatedSince); } - const user = Users.findOneById(this.userId, { fields: { _id: 1 } }); - const room = Rooms.findOneById(rid, { fields: { ...roomAccessAttributes, t: 1, _id: 1 } }); + const user = await Users.findOneById(this.userId, { projection: { _id: 1 } }); + const room = await Rooms.findOneById(rid, { projection: { ...roomAccessAttributes, t: 1, _id: 1 } }); - if (!canAccessRoom(room, user)) { + if (!room || !user || !canAccessRoom(room, user)) { throw new Meteor.Error('error-not-allowed', 'Not Allowed'); } const threadQuery = Object.assign({}, query, { rid, tcount: { $exists: true } }); return API.v1.success({ threads: { - update: Messages.find({ ...threadQuery, _updatedAt: { $gt: updatedSinceDate } }, { fields, sort }).fetch(), - remove: Messages.trashFindDeletedAfter(updatedSinceDate, threadQuery, { fields, sort }).fetch(), + update: await Messages.find( + { ...threadQuery, _updatedAt: { $gt: updatedSinceDate } }, + { + sort, + projection: fields, + }, + ).toArray(), + remove: await Messages.trashFindDeletedAfter(updatedSinceDate, threadQuery, { + sort, + projection: fields, + }).toArray(), }, }); }, @@ -590,17 +581,17 @@ API.v1.addRoute( if (!tmid) { throw new Meteor.Error('error-invalid-params', 'The required "tmid" query param is missing.'); } - const thread = Messages.findOneById(tmid, { fields: { rid: 1 } }); - if (!thread || !thread.rid) { + const thread = await Messages.findOneById(tmid, { projection: { rid: 1 } }); + if (!thread?.rid) { throw new Meteor.Error('error-invalid-message', 'Invalid Message'); } - const user = Users.findOneById(this.userId, { fields: { _id: 1 } }); - const room = Rooms.findOneById(thread.rid, { fields: { ...roomAccessAttributes, t: 1, _id: 1 } }); + const user = await Users.findOneById(this.userId, { projection: { _id: 1 } }); + const room = await Rooms.findOneById(thread.rid, { projection: { ...roomAccessAttributes, t: 1, _id: 1 } }); - if (!canAccessRoom(room, user)) { + if (!room || !user || !canAccessRoom(room, user)) { throw new Meteor.Error('error-not-allowed', 'Not Allowed'); } - const { cursor, totalCount } = MessagesRaw.findPaginated( + const { cursor, totalCount } = await Messages.findPaginated( { ...query, tmid }, { sort: sort || { ts: 1 }, @@ -626,12 +617,12 @@ API.v1.addRoute( 'chat.syncThreadMessages', { authRequired: true }, { - get() { + async get() { const { tmid } = this.queryParams; const { query, fields, sort } = this.parseJsonQuery(); const { updatedSince } = this.queryParams; let updatedSinceDate; - if (!settings.get('Threads_enabled')) { + if (!settings.get('Threads_enabled')) { throw new Meteor.Error('error-not-allowed', 'Threads Disabled'); } if (!tmid) { @@ -645,20 +636,20 @@ API.v1.addRoute( } else { updatedSinceDate = new Date(updatedSince); } - const thread = Messages.findOneById(tmid, { fields: { rid: 1 } }); - if (!thread || !thread.rid) { + const thread = await Messages.findOneById(tmid, { projection: { rid: 1 } }); + if (!thread?.rid) { throw new Meteor.Error('error-invalid-message', 'Invalid Message'); } - const user = Users.findOneById(this.userId, { fields: { _id: 1 } }); - const room = Rooms.findOneById(thread.rid, { fields: { ...roomAccessAttributes, t: 1, _id: 1 } }); + const user = await Users.findOneById(this.userId, { projection: { _id: 1 } }); + const room = await Rooms.findOneById(thread.rid, { projection: { ...roomAccessAttributes, t: 1, _id: 1 } }); - if (!canAccessRoom(room, user)) { + if (!room || !user || !canAccessRoom(room, user)) { throw new Meteor.Error('error-not-allowed', 'Not Allowed'); } return API.v1.success({ messages: { - update: Messages.find({ ...query, tmid, _updatedAt: { $gt: updatedSinceDate } }, { fields, sort }).fetch(), - remove: Messages.trashFindDeletedAfter(updatedSinceDate, { ...query, tmid }, { fields, sort }).fetch(), + update: await Messages.find({ ...query, tmid, _updatedAt: { $gt: updatedSinceDate } }, { projection: fields, sort }).toArray(), + remove: await Messages.trashFindDeletedAfter(updatedSinceDate, { ...query, tmid }, { projection: fields, sort }).toArray(), }, }); }, @@ -669,13 +660,15 @@ API.v1.addRoute( 'chat.followMessage', { authRequired: true }, { - post() { + async post() { const { mid } = this.bodyParams; if (!mid) { throw new Meteor.Error('The required "mid" body param is missing.'); } - Meteor.runAsUser(this.userId, () => Meteor.call('followMessage', { mid })); + + await Meteor.call('followMessage', { mid }); + return API.v1.success(); }, }, @@ -685,13 +678,15 @@ API.v1.addRoute( 'chat.unfollowMessage', { authRequired: true }, { - post() { + async post() { const { mid } = this.bodyParams; if (!mid) { throw new Meteor.Error('The required "mid" body param is missing.'); } - Meteor.runAsUser(this.userId, () => Meteor.call('unfollowMessage', { mid })); + + await Meteor.call('unfollowMessage', { mid }); + return API.v1.success(); }, }, @@ -701,24 +696,23 @@ API.v1.addRoute( 'chat.getMentionedMessages', { authRequired: true }, { - get() { + async get() { const { roomId } = this.queryParams; const { sort } = this.parseJsonQuery(); const { offset, count } = this.getPaginationItems(); if (!roomId) { throw new Meteor.Error('error-invalid-params', 'The required "roomId" query param is missing.'); } - const messages = Promise.await( - findMentionedMessages({ - uid: this.userId, - roomId, - pagination: { - offset, - count, - sort, - }, - }), - ); + const messages = await findMentionedMessages({ + uid: this.userId, + roomId, + pagination: { + offset, + count, + sort, + }, + }); + return API.v1.success(messages); }, }, @@ -728,7 +722,7 @@ API.v1.addRoute( 'chat.getStarredMessages', { authRequired: true }, { - get() { + async get() { const { roomId } = this.queryParams; const { sort } = this.parseJsonQuery(); const { offset, count } = this.getPaginationItems(); @@ -736,19 +730,17 @@ API.v1.addRoute( if (!roomId) { throw new Meteor.Error('error-invalid-params', 'The required "roomId" query param is missing.'); } - const messages = Promise.await( - findStarredMessages({ - uid: this.userId, - roomId, - pagination: { - offset, - count, - sort, - }, - }), - ); + const messages = await findStarredMessages({ + uid: this.userId, + roomId, + pagination: { + offset, + count, + sort, + }, + }); - messages.messages = normalizeMessagesForUser(messages.messages, this.userId); + messages.messages = await normalizeMessagesForUser(messages.messages, this.userId); return API.v1.success(messages); }, @@ -770,7 +762,7 @@ API.v1.addRoute( const messages = await findDiscussionsFromRoom({ uid: this.userId, roomId, - text, + text: text || '', pagination: { offset, count, diff --git a/apps/meteor/tests/end-to-end/api/05-chat.js b/apps/meteor/tests/end-to-end/api/05-chat.js index 2c4c7a98366..f119600fb4d 100644 --- a/apps/meteor/tests/end-to-end/api/05-chat.js +++ b/apps/meteor/tests/end-to-end/api/05-chat.js @@ -2662,6 +2662,7 @@ describe('Threads', () => { .set(credentials) .query({ tmid: threadMessage.tmid, + updatedSince: 'updatedSince', }) .expect('Content-Type', 'application/json') .expect(400) diff --git a/packages/rest-typings/src/index.ts b/packages/rest-typings/src/index.ts index 8558be1032c..8f4034c784f 100644 --- a/packages/rest-typings/src/index.ts +++ b/packages/rest-typings/src/index.ts @@ -247,3 +247,4 @@ export * from './v1/import'; export * from './v1/voip'; export * from './v1/email-inbox'; export * from './v1/federation'; +export * from './v1/chat'; diff --git a/packages/rest-typings/src/v1/chat.ts b/packages/rest-typings/src/v1/chat.ts index b1fefc99189..a51033ec317 100644 --- a/packages/rest-typings/src/v1/chat.ts +++ b/packages/rest-typings/src/v1/chat.ts @@ -1,6 +1,8 @@ -import type { IMessage, IRoom, ReadReceipt } from '@rocket.chat/core-typings'; +import type { IMessage, IRoom, MessageAttachment, ReadReceipt } from '@rocket.chat/core-typings'; import Ajv from 'ajv'; +import type { PaginatedRequest } from '../helpers/PaginatedRequest'; + const ajv = new Ajv({ coerceTypes: true, }); @@ -14,13 +16,60 @@ const chatSendMessageSchema = { properties: { message: { type: 'object', + properties: { + _id: { + type: 'string', + nullable: true, + }, + rid: { + type: 'string', + }, + tmid: { + type: 'string', + nullable: true, + }, + msg: { + type: 'string', + nullable: true, + }, + alias: { + type: 'string', + nullable: true, + }, + emoji: { + type: 'string', + nullable: true, + }, + tshow: { + type: 'boolean', + nullable: true, + }, + avatar: { + type: 'string', + nullable: true, + }, + attachments: { + type: 'array', + items: { + type: 'object', + }, + nullable: true, + }, + blocks: { + type: 'array', + items: { + type: 'object', + }, + nullable: true, + }, + }, }, }, required: ['message'], additionalProperties: false, }; -export const isChatSendMessageProps = ajv.compile(chatSendMessageSchema); +export const isChatSendMessageProps = ajv.compile(chatSendMessageSchema); type ChatFollowMessage = { mid: IMessage['_id']; @@ -74,51 +123,51 @@ const ChatGetMessageSchema = { export const isChatGetMessageProps = ajv.compile(ChatGetMessageSchema); type ChatStarMessage = { - msgId: IMessage['_id']; + messageId: IMessage['_id']; }; const ChatStarMessageSchema = { type: 'object', properties: { - msgId: { + messageId: { type: 'string', }, }, - required: ['msgId'], + required: ['messageId'], additionalProperties: false, }; export const isChatStarMessageProps = ajv.compile(ChatStarMessageSchema); type ChatUnstarMessage = { - msgId: IMessage['_id']; + messageId: IMessage['_id']; }; const ChatUnstarMessageSchema = { type: 'object', properties: { - msgId: { + messageId: { type: 'string', }, }, - required: ['msgId'], + required: ['messageId'], additionalProperties: false, }; export const isChatUnstarMessageProps = ajv.compile(ChatUnstarMessageSchema); type ChatPinMessage = { - msgId: IMessage['_id']; + messageId: IMessage['_id']; }; const ChatPinMessageSchema = { type: 'object', properties: { - msgId: { + messageId: { type: 'string', }, }, - required: ['msgId'], + required: ['messageId'], additionalProperties: false, }; @@ -141,12 +190,10 @@ const ChatUnpinMessageSchema = { export const isChatUnpinMessageProps = ajv.compile(ChatUnpinMessageSchema); -type ChatGetDiscussions = { +type ChatGetDiscussions = PaginatedRequest<{ roomId: IRoom['_id']; text?: string; - offset: number; - count: number; -}; +}>; const ChatGetDiscussionsSchema = { type: 'object', @@ -160,12 +207,14 @@ const ChatGetDiscussionsSchema = { }, offset: { type: 'number', + nullable: true, }, count: { type: 'number', + nullable: true, }, }, - required: ['roomId', 'offset', 'count'], + required: ['roomId'], additionalProperties: false, }; @@ -192,13 +241,11 @@ const ChatReportMessageSchema = { export const isChatReportMessageProps = ajv.compile(ChatReportMessageSchema); -type ChatGetThreadsList = { +type ChatGetThreadsList = PaginatedRequest<{ rid: IRoom['_id']; type: 'unread' | 'following' | 'all'; text?: string; - offset: number; - count: number; -}; +}>; const ChatGetThreadsListSchema = { type: 'object', @@ -208,6 +255,7 @@ const ChatGetThreadsListSchema = { }, type: { type: 'string', + nullable: true, }, text: { type: 'string', @@ -222,7 +270,7 @@ const ChatGetThreadsListSchema = { nullable: true, }, }, - required: ['rid', 'type'], + required: ['rid'], additionalProperties: false, }; @@ -252,6 +300,7 @@ export const isChatSyncThreadsListProps = ajv.compile(ChatS type ChatDelete = { msgId: IMessage['_id']; roomId: IRoom['_id']; + asUser?: boolean; }; const ChatDeleteSchema = { @@ -263,6 +312,10 @@ const ChatDeleteSchema = { roomId: { type: 'string', }, + asUser: { + type: 'boolean', + nullable: true, + }, }, required: ['msgId', 'roomId'], additionalProperties: false, @@ -270,7 +323,9 @@ const ChatDeleteSchema = { export const isChatDeleteProps = ajv.compile(ChatDeleteSchema); -type ChatReact = { emoji: string; messageId: IMessage['_id'] } | { reaction: string; messageId: IMessage['_id'] }; +type ChatReact = + | { emoji: string; messageId: IMessage['_id']; shouldReact?: boolean } + | { reaction: string; messageId: IMessage['_id']; shouldReact?: boolean }; const ChatReactSchema = { oneOf: [ @@ -283,6 +338,10 @@ const ChatReactSchema = { messageId: { type: 'string', }, + shouldReact: { + type: 'boolean', + nullable: true, + }, }, required: ['emoji', 'messageId'], additionalProperties: false, @@ -296,6 +355,10 @@ const ChatReactSchema = { messageId: { type: 'string', }, + shouldReact: { + type: 'boolean', + nullable: true, + }, }, required: ['reaction', 'messageId'], additionalProperties: false, @@ -334,12 +397,10 @@ const ChatIgnoreUserSchema = { export const isChatIgnoreUserProps = ajv.compile(ChatIgnoreUserSchema); -type ChatSearch = { +type ChatSearch = PaginatedRequest<{ roomId: IRoom['_id']; searchText: string; - count: number; - offset: number; -}; +}>; const ChatSearchSchema = { type: 'object', @@ -352,12 +413,14 @@ const ChatSearchSchema = { }, count: { type: 'number', + nullable: true, }, offset: { type: 'number', + nullable: true, }, }, - required: ['roomId', 'searchText', 'count', 'offset'], + required: ['roomId', 'searchText'], additionalProperties: false, }; @@ -405,35 +468,46 @@ const ChatGetMessageReadReceiptsSchema = { export const isChatGetMessageReadReceiptsProps = ajv.compile(ChatGetMessageReadReceiptsSchema); -type ChatPostMessage = { +type GetStarredMessages = { roomId: IRoom['_id']; - text?: string; + count?: number; + offset?: number; + sort?: string; }; -const ChatPostMessageSchema = { +const GetStarredMessagesSchema = { type: 'object', properties: { roomId: { type: 'string', }, - text: { + count: { + type: 'number', + nullable: true, + }, + offset: { + type: 'number', + nullable: true, + }, + sort: { type: 'string', + nullable: true, }, }, required: ['roomId'], additionalProperties: false, }; -export const isChatPostMessageSchemaProps = ajv.compile(ChatPostMessageSchema); +export const isChatGetStarredMessagesProps = ajv.compile(GetStarredMessagesSchema); -type GetStarredMessages = { +type GetPinnedMessages = { roomId: IRoom['_id']; count?: number; offset?: number; sort?: string; }; -const GetStarredMessagesSchema = { +const GetPinnedMessagesSchema = { type: 'object', properties: { roomId: { @@ -456,16 +530,16 @@ const GetStarredMessagesSchema = { additionalProperties: false, }; -export const isChatGetStarredMessagesPayload = ajv.compile(GetStarredMessagesSchema); +export const isChatGetPinnedMessagesProps = ajv.compile(GetPinnedMessagesSchema); -type GetPinnedMessages = { +type GetMentionedMessages = { roomId: IRoom['_id']; count?: number; offset?: number; sort?: string; }; -const GetPinnedMessagesSchema = { +const GetMentionedMessagesSchema = { type: 'object', properties: { roomId: { @@ -488,21 +562,43 @@ const GetPinnedMessagesSchema = { additionalProperties: false, }; -export const isChatGetPinnedMessagesPayload = ajv.compile(GetPinnedMessagesSchema); +export const isChatGetMentionedMessagesProps = ajv.compile(GetMentionedMessagesSchema); -type GetMentionedMessages = { +type ChatSyncMessages = { roomId: IRoom['_id']; - count?: number; - offset?: number; - sort?: string; + lastUpdate: string; }; -const GetMentionedMessagesSchema = { +const ChatSyncMessagesSchema = { type: 'object', properties: { roomId: { type: 'string', }, + lastUpdate: { + type: 'string', + }, + }, + required: ['roomId', 'lastUpdate'], + additionalProperties: false, +}; + +export const isChatSyncMessagesProps = ajv.compile(ChatSyncMessagesSchema); + +type ChatSyncThreadMessages = PaginatedRequest<{ + tmid: string; + updatedSince: string; +}>; + +const ChatSyncThreadMessagesSchema = { + type: 'object', + properties: { + tmid: { + type: 'string', + }, + updatedSince: { + type: 'string', + }, count: { type: 'number', nullable: true, @@ -516,15 +612,156 @@ const GetMentionedMessagesSchema = { nullable: true, }, }, - required: ['roomId'], + required: ['tmid', 'updatedSince'], + additionalProperties: false, +}; + +export const isChatSyncThreadMessagesProps = ajv.compile(ChatSyncThreadMessagesSchema); + +type ChatGetThreadMessages = PaginatedRequest<{ + tmid: string; +}>; + +const ChatGetThreadMessagesSchema = { + type: 'object', + properties: { + tmid: { + type: 'string', + }, + count: { + type: 'number', + nullable: true, + }, + offset: { + type: 'number', + nullable: true, + }, + sort: { + type: 'string', + nullable: true, + }, + }, + required: ['tmid'], + additionalProperties: false, +}; + +export const isChatGetThreadMessagesProps = ajv.compile(ChatGetThreadMessagesSchema); + +type ChatGetDeletedMessages = PaginatedRequest<{ + roomId: IRoom['_id']; + since: string; +}>; + +const ChatGetDeletedMessagesSchema = { + type: 'object', + properties: { + roomId: { + type: 'string', + }, + since: { + type: 'string', + }, + count: { + type: 'number', + nullable: true, + }, + offset: { + type: 'number', + nullable: true, + }, + sort: { + type: 'string', + nullable: true, + }, + }, + required: ['roomId', 'since'], additionalProperties: false, }; -export const isChatGetMentionedMessagesPayload = ajv.compile(GetMentionedMessagesSchema); +export const isChatGetDeletedMessagesProps = ajv.compile(ChatGetDeletedMessagesSchema); + +type ChatPostMessage = + | { roomId: string; text?: string; alias?: string; emoji?: string; avatar?: string; attachments?: MessageAttachment[] } + | { channel: string; text?: string; alias?: string; emoji?: string; avatar?: string; attachments?: MessageAttachment[] }; + +const ChatPostMessageSchema = { + oneOf: [ + { + type: 'object', + properties: { + roomId: { + type: 'string', + }, + text: { + type: 'string', + nullable: true, + }, + alias: { + type: 'string', + nullable: true, + }, + emoji: { + type: 'string', + nullable: true, + }, + avatar: { + type: 'string', + nullable: true, + }, + attachments: { + type: 'array', + items: { + type: 'object', + }, + nullable: true, + }, + }, + required: ['roomId'], + additionalProperties: false, + }, + { + type: 'object', + properties: { + channel: { + type: 'string', + }, + text: { + type: 'string', + nullable: true, + }, + alias: { + type: 'string', + nullable: true, + }, + emoji: { + type: 'string', + nullable: true, + }, + avatar: { + type: 'string', + nullable: true, + }, + attachments: { + type: 'array', + items: { + type: 'object', + }, + nullable: true, + }, + }, + required: ['channel'], + additionalProperties: false, + }, + ], +}; + +export const isChatPostMessageProps = ajv.compile(ChatPostMessageSchema); export type ChatEndpoints = { '/v1/chat.sendMessage': { - POST: (params: ChatSendMessage) => IMessage; + POST: (params: ChatSendMessage) => { + message: IMessage; + }; }; '/v1/chat.getMessage': { GET: (params: ChatGetMessage) => { @@ -544,7 +781,9 @@ export type ChatEndpoints = { POST: (params: ChatUnstarMessage) => void; }; '/v1/chat.pinMessage': { - POST: (params: ChatPinMessage) => void; + POST: (params: ChatPinMessage) => { + message: IMessage; + }; }; '/v1/chat.unPinMessage': { POST: (params: ChatUnpinMessage) => void; @@ -592,15 +831,12 @@ export type ChatEndpoints = { }; '/v1/chat.update': { POST: (params: ChatUpdate) => { - messages: IMessage; + message: IMessage; }; }; '/v1/chat.getMessageReadReceipts': { GET: (params: ChatGetMessageReadReceipts) => { receipts: ReadReceipt[] }; }; - '/v1/chat.postMessage': { - POST: (params: ChatPostMessage) => IMessage; - }; '/v1/chat.getStarredMessages': { GET: (params: GetStarredMessages) => { messages: IMessage[]; @@ -625,4 +861,43 @@ export type ChatEndpoints = { total: number; }; }; + '/v1/chat.syncMessages': { + GET: (params: ChatSyncMessages) => { + result: { + updated: IMessage[]; + deleted: IMessage[]; + }; + }; + }; + '/v1/chat.postMessage': { + POST: (params: ChatPostMessage) => { + ts: number; + channel: IRoom; + message: IMessage; + }; + }; + '/v1/chat.syncThreadMessages': { + GET: (params: ChatSyncThreadMessages) => { + messages: { + update: IMessage[]; + remove: IMessage[]; + }; + }; + }; + '/v1/chat.getThreadMessages': { + GET: (params: ChatGetThreadMessages) => { + messages: IMessage[]; + count: number; + offset: number; + total: number; + }; + }; + '/v1/chat.getDeletedMessages': { + GET: (params: ChatGetDeletedMessages) => { + messages: IMessage[]; + count: number; + offset: number; + total: number; + }; + }; };