From 97311da8aff2b6ed03c9b0bd42e85971126ca04f Mon Sep 17 00:00:00 2001 From: Diego Sampaio Date: Sat, 23 Jan 2021 04:55:56 -0300 Subject: [PATCH] [NEW][ENTERPRISE] Hide message history for new channel members (#20253) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Gabriel Thomé Co-authored-by: Pierre Lehnen Co-authored-by: Rodrigo Nascimento --- app/api/server/lib/messages.js | 41 ++-- app/api/server/v1/channels.js | 24 +- app/api/server/v1/chat.js | 68 +++--- app/api/server/v1/groups.js | 19 +- app/api/server/v1/im.js | 39 ++-- .../server/methods/saveRoomSettings.js | 14 +- .../creationDialog/CreateDiscussion.html | 16 ++ .../views/creationDialog/CreateDiscussion.js | 25 ++- .../server/methods/createDiscussion.js | 8 +- app/lib/lib/roomTypes/direct.js | 2 + app/lib/lib/roomTypes/private.js | 2 + app/lib/lib/roomTypes/public.js | 2 + app/lib/server/functions/cleanRoomHistory.js | 12 +- app/lib/server/functions/createDirectRoom.js | 4 + app/lib/server/functions/createRoom.js | 4 + .../server/functions/loadMessageHistory.js | 41 ++-- app/lib/server/methods/getChannelHistory.js | 39 ++-- app/livechat/lib/LivechatRoomType.js | 2 + app/livechat/server/hooks/sendToCRM.js | 4 +- .../methods/getUserMentionsByChannel.js | 11 +- .../server/unreadMessages.js | 2 +- app/models/server/models/Messages.js | 205 +++++++++-------- app/models/server/models/Rooms.js | 12 + app/models/server/raw/BaseRaw.ts | 9 + app/models/server/raw/Messages.js | 212 ++++++++++++++++-- .../server/methods/getThreadMessages.js | 10 +- app/threads/server/methods/getThreadsList.js | 5 +- app/ui/client/views/app/createChannel.html | 17 ++ app/ui/client/views/app/createChannel.js | 26 ++- app/utils/lib/RoomTypeConfig.js | 1 + .../Info/EditRoomInfo/EditRoomInfo.js | 18 ++ ee/app/license/client/index.ts | 21 ++ ee/app/message/service.ts | 185 +++++++++++++++ ee/client/hooks/useIsEnterprise.js | 18 ++ ee/server/index.js | 1 + ee/server/startup.ts | 6 + packages/rocketchat-i18n/i18n/en.i18n.json | 5 + packages/rocketchat-i18n/i18n/pt-BR.i18n.json | 5 + server/methods/loadHistory.js | 8 +- server/methods/loadMissedMessages.js | 11 +- server/methods/loadNextMessages.js | 14 +- server/methods/loadSurroundingMessages.js | 19 +- server/methods/messageSearch.js | 18 +- server/modules/watchers/publishFields.ts | 1 + server/publications/messages.js | 9 +- server/sdk/index.ts | 4 + server/sdk/types/IMessageEnterprise.ts | 3 + server/sdk/types/IMessageService.ts | 94 ++++++++ server/services/message/service.ts | 200 +++++++++++++++++ server/services/startup.ts | 2 + tests/end-to-end/api/05-chat.js | 4 +- 51 files changed, 1228 insertions(+), 294 deletions(-) create mode 100644 ee/app/message/service.ts create mode 100644 ee/client/hooks/useIsEnterprise.js create mode 100644 ee/server/startup.ts create mode 100644 server/sdk/types/IMessageEnterprise.ts create mode 100644 server/sdk/types/IMessageService.ts create mode 100644 server/services/message/service.ts diff --git a/app/api/server/lib/messages.js b/app/api/server/lib/messages.js index 257da349bb6e..fc33fba13a61 100644 --- a/app/api/server/lib/messages.js +++ b/app/api/server/lib/messages.js @@ -1,6 +1,7 @@ import { canAccessRoomAsync } from '../../../authorization/server/functions/canAccessRoom'; import { Rooms, Messages, Users } from '../../../models/server/raw'; import { getValue } from '../../../settings/server/raw'; +import { Message } from '../../../../server/sdk'; export async function findMentionedMessages({ uid, roomId, pagination: { offset, count, sort } }) { const room = await Rooms.findOneById(roomId); @@ -12,16 +13,16 @@ export async function findMentionedMessages({ uid, roomId, pagination: { offset, throw new Error('invalid-user'); } - const cursor = await Messages.findVisibleByMentionAndRoomId(user.username, roomId, { - sort: sort || { ts: -1 }, - skip: offset, - limit: count, + const { records: messages, total } = await Message.get(uid, { + rid: roomId, + mentionsUsername: user.username, + queryOptions: { + sort: sort || { ts: -1 }, + skip: offset, + limit: count, + }, }); - const total = await cursor.count(); - - const messages = await cursor.toArray(); - return { messages, count: messages.length, @@ -98,15 +99,13 @@ export async function findSnippetedMessages({ uid, roomId, pagination: { offset, throw new Error('error-not-allowed'); } - const cursor = await Messages.findSnippetedByRoom(roomId, { + const queryOptions = { sort: sort || { ts: -1 }, skip: offset, limit: count, - }); - - const total = await cursor.count(); + }; - const messages = await cursor.toArray(); + const { records: messages, total } = await Message.get(uid, { snippeted: true, queryOptions }); return { messages, @@ -123,16 +122,16 @@ export async function findDiscussionsFromRoom({ uid, roomId, text, pagination: { throw new Error('error-not-allowed'); } - const cursor = Messages.findDiscussionsByRoomAndText(roomId, text, { - sort: sort || { ts: -1 }, - skip: offset, - limit: count, + const { records: messages, total } = await Message.getDiscussions({ + rid: roomId, + text, + queryOptions: { + sort: sort || { ts: -1 }, + skip: offset, + limit: count, + }, }); - const total = await cursor.count(); - - const messages = await cursor.toArray(); - return { messages, count: messages.length, diff --git a/app/api/server/v1/channels.js b/app/api/server/v1/channels.js index 76af348d9a6c..4ac353773690 100644 --- a/app/api/server/v1/channels.js +++ b/app/api/server/v1/channels.js @@ -7,6 +7,7 @@ import { mountIntegrationQueryBasedOnPermissions } from '../../../integrations/s import { normalizeMessagesForUser } from '../../../utils/server/lib/normalizeMessagesForUser'; import { API } from '../api'; import { settings } from '../../../settings'; +import { Message } from '../../../../server/sdk'; // Returns the channel IF found otherwise it will return the failure of why it didn't. Check the `statusCode` property @@ -337,12 +338,12 @@ API.v1.addRoute('channels.history', { authRequired: true }, { get() { const findResult = findChannelByIdOrName({ params: this.requestParams(), checkedArchived: false }); - let latestDate = new Date(); + let latestDate; if (this.queryParams.latest) { latestDate = new Date(this.queryParams.latest); } - let oldestDate = undefined; + let oldestDate; if (this.queryParams.oldest) { oldestDate = new Date(this.queryParams.oldest); } @@ -576,15 +577,16 @@ API.v1.addRoute('channels.messages', { authRequired: true }, { return API.v1.unauthorized(); } - const cursor = Messages.find(ourQuery, { - sort: sort || { ts: -1 }, - skip: offset, - limit: count, - fields, - }); - - const total = cursor.count(); - const messages = cursor.fetch(); + const { records: messages, total } = Promise.await(Message.customQuery({ + query: ourQuery, + userId: this.userId, + queryOptions: { + sort: sort || { ts: -1 }, + skip: offset, + limit: count, + fields, + }, + })); return API.v1.success({ messages: normalizeMessagesForUser(messages, this.userId), diff --git a/app/api/server/v1/chat.js b/app/api/server/v1/chat.js index a4ed9765ebe4..52bb679846ef 100644 --- a/app/api/server/v1/chat.js +++ b/app/api/server/v1/chat.js @@ -2,6 +2,7 @@ import { Meteor } from 'meteor/meteor'; import { Match, check } from 'meteor/check'; import { Messages } from '../../../models'; +import { Message } from '../../../../server/sdk'; import { canAccessRoom, hasPermission } from '../../../authorization'; import { normalizeMessagesForUser } from '../../../utils/server/lib/normalizeMessagesForUser'; import { processWebhookMessage } from '../../../lib/server'; @@ -408,14 +409,14 @@ API.v1.addRoute('chat.getPinnedMessages', { authRequired: true }, { throw new Meteor.Error('error-not-allowed', 'Not allowed'); } - const cursor = Messages.findPinnedByRoom(room._id, { - skip: offset, - limit: count, - }); - - const total = cursor.count(); - - const messages = cursor.fetch(); + const { records: messages, total } = Promise.await(Message.get(this.userId, { + rid: room._id, + pinned: true, + queryOptions: { + skip: offset, + limit: count, + }, + })); return API.v1.success({ messages, @@ -456,16 +457,17 @@ API.v1.addRoute('chat.getThreadsList', { authRequired: true }, { }; const threadQuery = { ...query, ...typeThread, rid, tcount: { $exists: true } }; - const cursor = Messages.find(threadQuery, { - sort: sort || { tlm: -1 }, - skip: offset, - limit: count, - fields, - }); - - const total = cursor.count(); - const threads = cursor.fetch(); + const { records: threads, total } = Promise.await(Message.customQuery({ + query: threadQuery, + userId: this.userId, + queryOptions: { + sort: sort || { tlm: -1 }, + skip: offset, + limit: count, + fields, + }, + })); return API.v1.success({ threads, @@ -501,11 +503,12 @@ API.v1.addRoute('chat.syncThreadsList', { authRequired: true }, { if (!canAccessRoom(room, user)) { throw new Meteor.Error('error-not-allowed', 'Not Allowed'); } - const threadQuery = Object.assign({}, query, { rid, tcount: { $exists: true } }); + const threadQuery = { ...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: Promise.await(Message.customQuery({ query: { ...threadQuery, _updatedAt: { $gt: updatedSinceDate } }, queryOptions: { returnTotal: false, fields, sort } })).records, + remove: Promise.await(Message.getDeleted({ rid, userId: this.userId, timestamp: updatedSinceDate, query: threadQuery, queryOptions: { returnTotal: false, fields, sort } })).records, }, }); }, @@ -533,16 +536,19 @@ API.v1.addRoute('chat.getThreadMessages', { authRequired: true }, { if (!canAccessRoom(room, user)) { throw new Meteor.Error('error-not-allowed', 'Not Allowed'); } - const cursor = Messages.find({ ...query, tmid }, { - sort: sort || { ts: 1 }, - skip: offset, - limit: count, - fields, - }); - const total = cursor.count(); + const ourQuery = Object.assign({}, query, { rid: thread.rid, tmid }); - const messages = cursor.fetch(); + const { records: messages, total } = Promise.await(Message.customQuery({ + query: ourQuery, + userId: this.userId, + queryOptions: { + sort: sort || { ts: 1 }, + skip: offset, + limit: count, + fields, + }, + })); return API.v1.success({ messages, @@ -573,6 +579,7 @@ API.v1.addRoute('chat.syncThreadMessages', { authRequired: true }, { } else { updatedSinceDate = new Date(updatedSince); } + const thread = Messages.findOneById(tmid, { fields: { rid: 1 } }); if (!thread || !thread.rid) { throw new Meteor.Error('error-invalid-message', 'Invalid Message'); @@ -583,10 +590,11 @@ API.v1.addRoute('chat.syncThreadMessages', { authRequired: true }, { if (!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: Promise.await(Message.customQuery({ query: { ...query, tmid, _updatedAt: { $gt: updatedSinceDate } }, queryOptions: { returnTotal: false, fields, sort } })).records, + remove: Promise.await(Message.getDeleted({ rid: thread.rid, timestamp: updatedSinceDate, query: { ...query, tmid }, queryOptions: { returnTotal: false, fields, sort } })).records, }, }); }, diff --git a/app/api/server/v1/groups.js b/app/api/server/v1/groups.js index ba5002f8bbb4..9d2c4e5a8f92 100644 --- a/app/api/server/v1/groups.js +++ b/app/api/server/v1/groups.js @@ -7,6 +7,7 @@ import { Subscriptions, Rooms, Messages, Uploads, Integrations, Users } from '.. import { hasPermission, hasAtLeastOnePermission, canAccessRoom } from '../../../authorization/server'; import { normalizeMessagesForUser } from '../../../utils/server/lib/normalizeMessagesForUser'; import { API } from '../api'; +import { Message } from '../../../../server/sdk'; // Returns the private group subscription IF found otherwise it will return the failure of why it didn't. Check the `statusCode` property export function findPrivateGroupByIdOrName({ params, userId, checkedArchived = true }) { @@ -525,18 +526,22 @@ API.v1.addRoute('groups.messages', { authRequired: true }, { const ourQuery = Object.assign({}, query, { rid: findResult.rid }); - const messages = Messages.find(ourQuery, { - sort: sort || { ts: -1 }, - skip: offset, - limit: count, - fields, - }).fetch(); + const { records: messages, total } = Promise.await(Message.customQuery({ + query: ourQuery, + userId: this.userId, + queryOptions: { + sort: sort || { ts: -1 }, + skip: offset, + limit: count, + fields, + }, + })); return API.v1.success({ messages: normalizeMessagesForUser(messages, this.userId), count: messages.length, offset, - total: Messages.find(ourQuery).count(), + total, }); }, }); diff --git a/app/api/server/v1/im.js b/app/api/server/v1/im.js index f61426949b08..a1c18b2023bc 100644 --- a/app/api/server/v1/im.js +++ b/app/api/server/v1/im.js @@ -1,11 +1,12 @@ import { Meteor } from 'meteor/meteor'; -import { Subscriptions, Uploads, Users, Messages, Rooms } from '../../../models'; +import { Subscriptions, Uploads, Users, Rooms } from '../../../models'; import { hasPermission } from '../../../authorization'; import { normalizeMessagesForUser } from '../../../utils/server/lib/normalizeMessagesForUser'; import { settings } from '../../../settings'; import { API } from '../api'; import { getDirectMessageByNameOrIdWithOptionToJoin } from '../../../lib/server/functions/getDirectMessageByNameOrIdWithOptionToJoin'; +import { Message } from '../../../../server/sdk'; function findDirectMessageRoom(params, user) { if ((!params.roomId || !params.roomId.trim()) && (!params.username || !params.username.trim())) { @@ -234,18 +235,22 @@ API.v1.addRoute(['dm.messages', 'im.messages'], { authRequired: true }, { const ourQuery = Object.assign({}, query, { rid: findResult.room._id }); - const messages = Messages.find(ourQuery, { - sort: sort || { ts: -1 }, - skip: offset, - limit: count, - fields, - }).fetch(); + const { records: messages, total } = Promise.await(Message.customQuery({ + query: ourQuery, + userId: this.userId, + queryOptions: { + sort: sort || { ts: -1 }, + skip: offset, + limit: count, + fields, + }, + })); return API.v1.success({ messages: normalizeMessagesForUser(messages, this.userId), count: messages.length, offset, - total: Messages.find(ourQuery).count(), + total, }); }, }); @@ -274,18 +279,22 @@ API.v1.addRoute(['dm.messages.others', 'im.messages.others'], { authRequired: tr const { sort, fields, query } = this.parseJsonQuery(); const ourQuery = Object.assign({}, query, { rid: room._id }); - const msgs = Messages.find(ourQuery, { - sort: sort || { ts: -1 }, - skip: offset, - limit: count, - fields, - }).fetch(); + const { records: msgs, total } = Promise.await(Message.customQuery({ + query: ourQuery, + userId: this.userId, + queryOptions: { + sort: sort || { ts: -1 }, + skip: offset, + limit: count, + fields, + }, + })); return API.v1.success({ messages: normalizeMessagesForUser(msgs, this.userId), offset, count: msgs.length, - total: Messages.find(ourQuery).count(), + total, }); }, }); diff --git a/app/channel-settings/server/methods/saveRoomSettings.js b/app/channel-settings/server/methods/saveRoomSettings.js index e524637e8b96..c89a99aaba5d 100644 --- a/app/channel-settings/server/methods/saveRoomSettings.js +++ b/app/channel-settings/server/methods/saveRoomSettings.js @@ -18,8 +18,9 @@ import { saveRoomTokenpass } from '../functions/saveRoomTokens'; import { saveRoomEncrypted } from '../functions/saveRoomEncrypted'; import { saveStreamingOptions } from '../functions/saveStreamingOptions'; import { RoomSettingsEnum, roomTypes } from '../../../utils'; +import { isEnterprise } from '../../../../ee/app/license/server/license'; -const fields = ['roomAvatar', 'featured', 'roomName', 'roomTopic', 'roomAnnouncement', 'roomCustomFields', 'roomDescription', 'roomType', 'readOnly', 'reactWhenReadOnly', 'systemMessages', 'default', 'joinCode', 'tokenpass', 'streamingOptions', 'retentionEnabled', 'retentionMaxAge', 'retentionExcludePinned', 'retentionFilesOnly', 'retentionIgnoreThreads', 'retentionOverrideGlobal', 'encrypted', 'favorite']; +const fields = ['roomAvatar', 'featured', 'roomName', 'roomTopic', 'roomAnnouncement', 'roomCustomFields', 'roomDescription', 'roomType', 'readOnly', 'reactWhenReadOnly', 'systemMessages', 'default', 'joinCode', 'tokenpass', 'streamingOptions', 'retentionEnabled', 'retentionMaxAge', 'retentionExcludePinned', 'retentionFilesOnly', 'retentionIgnoreThreads', 'retentionOverrideGlobal', 'encrypted', 'favorite', 'hideHistoryForNewMembers']; const validators = { default({ userId }) { @@ -122,6 +123,14 @@ const validators = { }); } }, + hideHistoryForNewMembers({ value }) { + if (!isEnterprise() && value) { + throw new Meteor.Error('error-action-not-allowed', 'Hiding the history for new users is not allowed.', { + method: 'saveRoomSettings', + action: 'Editing_room', + }); + } + }, }; const settingSavers = { @@ -217,6 +226,9 @@ const settingSavers = { roomAvatar({ value, rid, user }) { setRoomAvatar(rid, value, user); }, + hideHistoryForNewMembers({ value, rid }) { + Rooms.saveHideHistoryForNewMembers(rid, value); + }, }; Meteor.methods({ diff --git a/app/discussion/client/views/creationDialog/CreateDiscussion.html b/app/discussion/client/views/creationDialog/CreateDiscussion.html index 9eff9c8e57ad..fe0cda2f1dd1 100644 --- a/app/discussion/client/views/creationDialog/CreateDiscussion.html +++ b/app/discussion/client/views/creationDialog/CreateDiscussion.html @@ -43,6 +43,22 @@ + {{#if hideHistoryVisible}} +
+ + + {{hideHistoryDescription}} + +
+ {{/if}}
{{/if}} + {{#if hideHistoryVisible}} +
+ + + {{hideHistoryDescription}} + +
+ {{/if}} +