diff --git a/app/api/server/lib/messages.js b/app/api/server/lib/messages.js new file mode 100644 index 0000000000000..2b2d9a9c23ef9 --- /dev/null +++ b/app/api/server/lib/messages.js @@ -0,0 +1,30 @@ +import { canAccessRoomAsync } from '../../../authorization/server/functions/canAccessRoom'; +import { Rooms, Messages, Users } from '../../../models/server/raw'; + +export async function findMentionedMessages({ uid, roomId, pagination: { offset, count, sort } }) { + const room = await Rooms.findOneById(roomId); + if (!await canAccessRoomAsync(room, { _id: uid })) { + throw new Error('error-not-allowed'); + } + const user = await Users.findOneById(uid, { fields: { username: 1 } }); + if (!user) { + throw new Error('invalid-user'); + } + + const cursor = await Messages.findVisibleByMentionAndRoomId(user.username, roomId, { + sort: sort || { ts: -1 }, + skip: offset, + limit: count, + }); + + const total = await cursor.count(); + + const messages = await cursor.toArray(); + + return { + messages, + count: messages.length, + offset, + total, + }; +} diff --git a/app/api/server/v1/chat.js b/app/api/server/v1/chat.js index 50b7eeb9d9ec6..b015651f0285c 100644 --- a/app/api/server/v1/chat.js +++ b/app/api/server/v1/chat.js @@ -9,6 +9,7 @@ import { API } from '../api'; import Rooms from '../../../models/server/models/Rooms'; import Users from '../../../models/server/models/Users'; import { settings } from '../../../settings'; +import { findMentionedMessages } from '../lib/messages'; API.v1.addRoute('chat.delete', { authRequired: true }, { post() { @@ -599,3 +600,24 @@ API.v1.addRoute('chat.unfollowMessage', { authRequired: true }, { return API.v1.success(); }, }); + +API.v1.addRoute('chat.getMentionedMessages', { authRequired: true }, { + 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, + }, + })); + return API.v1.success(messages); + }, +}); diff --git a/app/file-upload/server/lib/FileUpload.js b/app/file-upload/server/lib/FileUpload.js index 4654c9f197e80..8fb538c5b64dc 100644 --- a/app/file-upload/server/lib/FileUpload.js +++ b/app/file-upload/server/lib/FileUpload.js @@ -24,6 +24,7 @@ import { hasPermission } from '../../../authorization/server/functions/hasPermis import { canAccessRoom } from '../../../authorization/server/functions/canAccessRoom'; import { fileUploadIsValidContentType } from '../../../utils/lib/fileUploadRestrictions'; import { isValidJWT, generateJWT } from '../../../utils/server/lib/JWTHelper'; +import { Messages } from '../../../models/server'; const cookie = new Cookies(); let maxFileSize = 0; @@ -403,6 +404,19 @@ export const FileUpload = { fileId, }, settings.get('FileUpload_json_web_token_secret_for_files')); }, + + removeFilesByRoomId(rid) { + Messages.find({ + rid, + 'file._id': { + $exists: true, + }, + }, { + fields: { + 'file._id': 1, + }, + }).fetch().forEach((document) => FileUpload.getStore('Uploads').deleteById(document.file._id)); + }, }; export class FileUploadClass { diff --git a/app/lib/server/functions/deleteRoom.js b/app/lib/server/functions/deleteRoom.js index a00d11d0acfab..dab074e4ccf54 100644 --- a/app/lib/server/functions/deleteRoom.js +++ b/app/lib/server/functions/deleteRoom.js @@ -1,8 +1,9 @@ import { Messages, Subscriptions, Rooms } from '../../../models'; import { callbacks } from '../../../callbacks'; +import { FileUpload } from '../../../file-upload/server'; export const deleteRoom = function(rid) { - Messages.removeFilesByRoomId(rid); + FileUpload.removeFilesByRoomId(rid); Messages.removeByRoomId(rid); callbacks.run('beforeDeleteRoom', rid); Subscriptions.removeByRoomId(rid); diff --git a/app/lib/server/functions/deleteUser.js b/app/lib/server/functions/deleteUser.js index 5f127edb1bf50..031ce3c612eb4 100644 --- a/app/lib/server/functions/deleteUser.js +++ b/app/lib/server/functions/deleteUser.js @@ -86,7 +86,7 @@ export const deleteUser = function(userId) { // Remove DMs and non-channel rooms with only 1 user (the one being deleted) if (roomData.t === 'd' || (roomData.t !== 'c' && roomData.subscribers === 1)) { Subscriptions.removeByRoomId(roomData.rid); - Messages.removeFilesByRoomId(roomData.rid); + FileUpload.removeFilesByRoomId(roomData.rid); Messages.removeByRoomId(roomData.rid); Rooms.removeById(roomData.rid); } diff --git a/app/livechat/server/lib/Livechat.js b/app/livechat/server/lib/Livechat.js index 6a9bca7bd8a33..530b83e7f21f5 100644 --- a/app/livechat/server/lib/Livechat.js +++ b/app/livechat/server/lib/Livechat.js @@ -35,6 +35,7 @@ import { LivechatInquiry } from '../../lib/LivechatInquiry'; import { sendMessage } from '../../../lib/server/functions/sendMessage'; import { updateMessage } from '../../../lib/server/functions/updateMessage'; import { deleteMessage } from '../../../lib/server/functions/deleteMessage'; +import { FileUpload } from '../../../file-upload/server'; export const Livechat = { Analytics, @@ -635,7 +636,7 @@ export const Livechat = { check(token, String); LivechatRooms.findByVisitorToken(token).forEach((room) => { - Messages.removeFilesByRoomId(room._id); + FileUpload.removeFilesByRoomId(room._id); Messages.removeByRoomId(room._id); }); diff --git a/app/mentions-flextab/client/lib/MentionedMessage.js b/app/mentions-flextab/client/lib/MentionedMessage.js deleted file mode 100644 index 563e2344c1029..0000000000000 --- a/app/mentions-flextab/client/lib/MentionedMessage.js +++ /dev/null @@ -1,3 +0,0 @@ -import { Mongo } from 'meteor/mongo'; - -export const MentionedMessage = new Mongo.Collection('rocketchat_mentioned_message'); diff --git a/app/mentions-flextab/client/views/mentionsFlexTab.js b/app/mentions-flextab/client/views/mentionsFlexTab.js index ed3fad676e9f7..b1b09030ba98d 100644 --- a/app/mentions-flextab/client/views/mentionsFlexTab.js +++ b/app/mentions-flextab/client/views/mentionsFlexTab.js @@ -1,16 +1,23 @@ import _ from 'underscore'; +import { Meteor } from 'meteor/meteor'; +import { Mongo } from 'meteor/mongo'; import { ReactiveVar } from 'meteor/reactive-var'; import { Template } from 'meteor/templating'; -import { MentionedMessage } from '../lib/MentionedMessage'; import { messageContext } from '../../../ui-utils/client/lib/messageContext'; +import { upsertMessageBulk } from '../../../ui-utils/client/lib/RoomHistoryManager'; +import { APIClient } from '../../../utils/client'; +import { Messages, Users } from '../../../models/client'; + +const LIMIT_DEFAULT = 50; Template.mentionsFlexTab.helpers({ hasMessages() { - return Template.instance().cursor.count() > 0; + return Template.instance().messages.find().count(); }, messages() { - return Template.instance().cursor; + const instance = Template.instance(); + return instance.messages.find({}, { limit: instance.limit.get(), sort: { ts: -1 } }); }, hasMore() { return Template.instance().hasMore.get(); @@ -19,23 +26,50 @@ Template.mentionsFlexTab.helpers({ }); Template.mentionsFlexTab.onCreated(function() { - this.cursor = MentionedMessage.find({ - rid: this.data.rid, - }, { - sort: { - ts: -1, - }, - }); + this.messages = new Mongo.Collection(null); + this.hasMore = new ReactiveVar(true); - this.limit = new ReactiveVar(50); - return this.autorun(() => { - const mentionedMessageFind = MentionedMessage.find({ rid: this.data.rid }); - return this.subscribe('mentionedMessages', this.data.rid, this.limit.get(), () => { - if (mentionedMessageFind.count() < this.limit.get()) { - return this.hasMore.set(false); - } + this.limit = new ReactiveVar(LIMIT_DEFAULT); + + this.autorun(() => { + const query = { + _hidden: { $ne: true }, + 'mentions.username': Users.findOne(Meteor.userId(), { fields: { username: 1 } }).username, + rid: this.data.rid, + _updatedAt: { + $gt: new Date(), + }, + }; + + this.cursor && this.cursor.stop(); + + this.limit.set(LIMIT_DEFAULT); + + this.cursor = Messages.find(query).observe({ + added: ({ _id, ...message }) => { + this.messages.upsert({ _id }, message); + }, + changed: ({ _id, ...message }) => { + this.messages.upsert({ _id }, message); + }, + removed: ({ _id }) => { + this.messages.remove({ _id }); + }, }); }); + + this.autorun(async () => { + const limit = this.limit.get(); + const { messages, total } = await APIClient.v1.get(`chat.getMentionedMessages?roomId=${ this.data.rid }&count=${ limit }`); + + upsertMessageBulk({ msgs: messages }, this.messages); + + this.hasMore.set(total > limit); + }); +}); + +Template.mentionsFlexTab.onDestroyed(function() { + this.cursor.stop(); }); Template.mentionsFlexTab.events({ diff --git a/app/mentions-flextab/server/publications/mentionedMessages.js b/app/mentions-flextab/server/publications/mentionedMessages.js index 8eaa292347e00..1fd23b563604d 100644 --- a/app/mentions-flextab/server/publications/mentionedMessages.js +++ b/app/mentions-flextab/server/publications/mentionedMessages.js @@ -3,6 +3,7 @@ import { Meteor } from 'meteor/meteor'; import { Users, Messages } from '../../../models'; Meteor.publish('mentionedMessages', function(rid, limit = 50) { + console.warn('The publication "mentionedMessages" is deprecated and will be removed after version v3.0.0'); if (!this.userId) { return this.ready(); } diff --git a/app/models/server/models/Messages.js b/app/models/server/models/Messages.js index 0489bc85226c2..71515b51271ac 100644 --- a/app/models/server/models/Messages.js +++ b/app/models/server/models/Messages.js @@ -4,7 +4,6 @@ import _ from 'underscore'; import { Base } from './_Base'; import Rooms from './Rooms'; import { settings } from '../../../settings/server/functions/settings'; -import { FileUpload } from '../../../file-upload/server/lib/FileUpload'; export class Messages extends Base { constructor() { @@ -905,19 +904,6 @@ export class Messages extends Base { return this.remove(query); } - async removeFilesByRoomId(roomId) { - this.find({ - rid: roomId, - 'file._id': { - $exists: true, - }, - }, { - fields: { - 'file._id': 1, - }, - }).fetch().forEach((document) => FileUpload.getStore('Uploads').deleteById(document.file._id)); - } - getMessageByFileId(fileID) { return this.findOne({ 'file._id': fileID }); } diff --git a/app/models/server/raw/Messages.js b/app/models/server/raw/Messages.js new file mode 100644 index 0000000000000..62496b4a8d346 --- /dev/null +++ b/app/models/server/raw/Messages.js @@ -0,0 +1,13 @@ +import { BaseRaw } from './BaseRaw'; + +export class MessagesRaw extends BaseRaw { + findVisibleByMentionAndRoomId(username, rid, options) { + const query = { + _hidden: { $ne: true }, + 'mentions.username': username, + rid, + }; + + return this.find(query, options); + } +} diff --git a/app/models/server/raw/index.js b/app/models/server/raw/index.js index 50456b5a61cd0..12b7ec7f556f9 100644 --- a/app/models/server/raw/index.js +++ b/app/models/server/raw/index.js @@ -14,6 +14,8 @@ import LivechatDepartmentModel from '../models/LivechatDepartment'; import { LivechatDepartmentRaw } from './LivechatDepartment'; import LivechatDepartmentAgentsModel from '../models/LivechatDepartmentAgents'; import { LivechatDepartmentAgentsRaw } from './LivechatDepartmentAgents'; +import MessagesModel from '../models/Messages'; +import { MessagesRaw } from './Messages'; export const Permissions = new PermissionsRaw(PermissionsModel.model.rawCollection()); export const Roles = new RolesRaw(RolesModel.model.rawCollection()); @@ -23,3 +25,4 @@ export const Users = new UsersRaw(UsersModel.model.rawCollection()); export const Rooms = new RoomsRaw(RoomsModel.model.rawCollection()); export const LivechatDepartment = new LivechatDepartmentRaw(LivechatDepartmentModel.model.rawCollection()); export const LivechatDepartmentAgents = new LivechatDepartmentAgentsRaw(LivechatDepartmentAgentsModel.model.rawCollection()); +export const Messages = new MessagesRaw(MessagesModel.model.rawCollection()); diff --git a/server/startup/migrations/v131.js b/server/startup/migrations/v131.js index 0d047991e8f69..fb895ca9030d8 100644 --- a/server/startup/migrations/v131.js +++ b/server/startup/migrations/v131.js @@ -1,5 +1,6 @@ import { Migrations } from '../../../app/migrations'; import { Users, Subscriptions, Rooms, Messages } from '../../../app/models'; +import { FileUpload } from '../../../app/file-upload/server'; Migrations.add({ version: 131, @@ -35,7 +36,7 @@ Migrations.add({ // Remove direct messages and also non-channel rooms with only 1 user (the one being deleted) if (room.t === 'd' || (room.t !== 'c' && Subscriptions.findByRoomId(room._id).count() === 1)) { Subscriptions.removeByRoomId(subscription.rid); - Messages.removeFilesByRoomId(subscription.rid); + FileUpload.removeFilesByRoomId(subscription.rid); Messages.removeByRoomId(subscription.rid); Rooms.removeById(subscription.rid); } diff --git a/tests/end-to-end/api/05-chat.js b/tests/end-to-end/api/05-chat.js index c626cae609ed9..a3a12f3cc5cd8 100644 --- a/tests/end-to-end/api/05-chat.js +++ b/tests/end-to-end/api/05-chat.js @@ -2076,4 +2076,45 @@ describe('Threads', () => { }); }); }); + + describe('[/chat.getMentionedMessages]', () => { + it('should return an error when the required "roomId" parameter is not sent', (done) => { + request.get(api('chat.getMentionedMessages')) + .set(credentials) + .expect('Content-Type', 'application/json') + .expect(400) + .expect((res) => { + expect(res.body).to.have.property('success', false); + expect(res.body.errorType).to.be.equal('error-invalid-params'); + }) + .end(done); + }); + + it('should return an error when the roomId is invalid', (done) => { + request.get(api('chat.getMentionedMessages?roomId=invalid-room')) + .set(credentials) + .expect('Content-Type', 'application/json') + .expect(400) + .expect((res) => { + expect(res.body).to.have.property('success', false); + expect(res.body.error).to.be.equal('error-not-allowed'); + }) + .end(done); + }); + + it('should return the mentioned messages', (done) => { + request.get(api('chat.getMentionedMessages?roomId=GENERAL')) + .set(credentials) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body.messages).to.be.an('array'); + expect(res.body).to.have.property('offset'); + expect(res.body).to.have.property('total'); + expect(res.body).to.have.property('count'); + }) + .end(done); + }); + }); });