diff --git a/services/chat/app/js/Features/Messages/MessageFormatter.js b/services/chat/app/js/Features/Messages/MessageFormatter.js index ccad55d1a7..373087aea5 100644 --- a/services/chat/app/js/Features/Messages/MessageFormatter.js +++ b/services/chat/app/js/Features/Messages/MessageFormatter.js @@ -1,65 +1,66 @@ -let MessageFormatter -module.exports = MessageFormatter = { - formatMessageForClientSide(message) { - if (message._id) { - message.id = message._id.toString() - delete message._id - } - const formattedMessage = { - id: message.id, - content: message.content, - timestamp: message.timestamp, - user_id: message.user_id, - } - if (message.edited_at) { - formattedMessage.edited_at = message.edited_at - } - return formattedMessage - }, - - formatMessagesForClientSide(messages) { - return messages.map(message => this.formatMessageForClientSide(message)) - }, - - groupMessagesByThreads(rooms, messages) { - let room, thread - const roomsById = {} - for (room of rooms) { - roomsById[room._id.toString()] = room - } - - const threads = {} - const getThread = function (room) { - const threadId = room.thread_id.toString() - if (threads[threadId]) { - return threads[threadId] - } else { - const thread = { messages: [] } - if (room.resolved) { - thread.resolved = true - thread.resolved_at = room.resolved.ts - thread.resolved_by_user_id = room.resolved.user_id - } - threads[threadId] = thread - return thread - } - } - - for (const message of messages) { - room = roomsById[message.room_id.toString()] - if (room) { - thread = getThread(room) - thread.messages.push( - MessageFormatter.formatMessageForClientSide(message) - ) - } - } - - for (const threadId in threads) { - thread = threads[threadId] - thread.messages.sort((a, b) => a.timestamp - b.timestamp) - } - - return threads - }, +function formatMessageForClientSide(message) { + if (message._id) { + message.id = message._id.toString() + delete message._id + } + const formattedMessage = { + id: message.id, + content: message.content, + timestamp: message.timestamp, + user_id: message.user_id, + } + if (message.edited_at) { + formattedMessage.edited_at = message.edited_at + } + return formattedMessage +} + +function formatMessagesForClientSide(messages) { + return messages.map(message => formatMessageForClientSide(message)) +} + +function groupMessagesByThreads(rooms, messages) { + let room, thread + const roomsById = {} + for (room of rooms) { + roomsById[room._id.toString()] = room + } + + const threads = {} + const getThread = function (room) { + const threadId = room.thread_id.toString() + if (threads[threadId]) { + return threads[threadId] + } else { + const thread = { messages: [] } + if (room.resolved) { + thread.resolved = true + thread.resolved_at = room.resolved.ts + thread.resolved_by_user_id = room.resolved.user_id + } + threads[threadId] = thread + return thread + } + } + + for (const message of messages) { + room = roomsById[message.room_id.toString()] + if (room) { + thread = getThread(room) + thread.messages.push(formatMessageForClientSide(message)) + } + } + + for (const threadId in threads) { + thread = threads[threadId] + thread.messages.sort((a, b) => a.timestamp - b.timestamp) + } + + return threads +} + +module.exports = { + formatMessagesForClientSide, + formatMessageForClientSide, + groupMessagesByThreads, } diff --git a/services/chat/app/js/Features/Messages/MessageHttpController.js b/services/chat/app/js/Features/Messages/MessageHttpController.js index 69b74fa298..7f23b71f3c 100644 --- a/services/chat/app/js/Features/Messages/MessageHttpController.js +++ b/services/chat/app/js/Features/Messages/MessageHttpController.js @@ -1,236 +1,221 @@ -let MessageHttpController const logger = require('@overleaf/logger') const MessageManager = require('./MessageManager') const MessageFormatter = require('./MessageFormatter') const ThreadManager = require('../Threads/ThreadManager') const { ObjectId } = require('../../mongodb') -module.exports = MessageHttpController = { - DEFAULT_MESSAGE_LIMIT: 50, - MAX_MESSAGE_LENGTH: 10 * 1024, // 10kb, about 1,500 words +const DEFAULT_MESSAGE_LIMIT = 50 +const MAX_MESSAGE_LENGTH = 10 * 1024 // 10kb, about 1,500 words - getGlobalMessages(req, res, next) { - MessageHttpController._getMessages( - ThreadManager.GLOBAL_THREAD, - req, - res, - next - ) - }, +function getGlobalMessages(req, res, next) { + _getMessages(ThreadManager.GLOBAL_THREAD, req, res, next) +} - sendGlobalMessage(req, res, next) { - MessageHttpController._sendMessage( - ThreadManager.GLOBAL_THREAD, - req, - res, - next - ) - }, +function sendGlobalMessage(req, res, next) { + _sendMessage(ThreadManager.GLOBAL_THREAD, req, res, next) +} - sendThreadMessage(req, res, next) { - MessageHttpController._sendMessage(req.params.threadId, req, res, next) - }, +function sendThreadMessage(req, res, next) { + _sendMessage(req.params.threadId, req, res, next) +} - getAllThreads(req, res, next) { - const { projectId } = req.params - logger.log({ projectId }, 'getting all threads') - ThreadManager.findAllThreadRooms(projectId, function (error, rooms) { +function getAllThreads(req, res, next) { + const { projectId } = req.params + logger.log({ projectId }, 'getting all threads') + ThreadManager.findAllThreadRooms(projectId, function (error, rooms) { + if (error) { + return next(error) + } + const roomIds = rooms.map(r => r._id) + MessageManager.findAllMessagesInRooms(roomIds, function (error, messages) { if (error) { return next(error) } - const roomIds = rooms.map(r => r._id) - MessageManager.findAllMessagesInRooms( - roomIds, - function (error, messages) { - if (error) { - return next(error) - } - const threads = MessageFormatter.groupMessagesByThreads( - rooms, - messages - ) - res.json(threads) - } - ) + const threads = MessageFormatter.groupMessagesByThreads(rooms, messages) + res.json(threads) }) - }, + }) +} - resolveThread(req, res, next) { - const { projectId, threadId } = req.params - const { user_id: userId } = req.body - logger.log({ userId, projectId, threadId }, 'marking thread as resolved') - ThreadManager.resolveThread(projectId, threadId, userId, function (error) { +function resolveThread(req, res, next) { + const { projectId, threadId } = req.params + const { user_id: userId } = req.body + logger.log({ userId, projectId, threadId }, 'marking thread as resolved') + ThreadManager.resolveThread(projectId, threadId, userId, function (error) { + if (error) { + return next(error) + } + res.sendStatus(204) + }) +} + +function reopenThread(req, res, next) { + const { projectId, threadId } = req.params + logger.log({ projectId, threadId }, 'reopening thread') + ThreadManager.reopenThread(projectId, threadId, function (error) { + if (error) { + return next(error) + } + res.sendStatus(204) + }) +} + +function deleteThread(req, res, next) { + const { projectId, threadId } = req.params + logger.log({ projectId, threadId }, 'deleting thread') + ThreadManager.deleteThread(projectId, threadId, function (error, roomId) { + if (error) { + return next(error) + } + MessageManager.deleteAllMessagesInRoom(roomId, function (error) { if (error) { return next(error) } res.sendStatus(204) }) - }, // No content + }) +} - reopenThread(req, res, next) { - const { projectId, threadId } = req.params - logger.log({ projectId, threadId }, 'reopening thread') - ThreadManager.reopenThread(projectId, threadId, function (error) { - if (error) { - return next(error) - } - res.sendStatus(204) - }) - }, // No content - - deleteThread(req, res, next) { - const { projectId, threadId } = req.params - logger.log({ projectId, threadId }, 'deleting thread') - ThreadManager.deleteThread(projectId, threadId, function (error, roomId) { - if (error) { - return next(error) - } - MessageManager.deleteAllMessagesInRoom(roomId, function (error) { +function editMessage(req, res, next) { + const { content } = req.body + const { projectId, threadId, messageId } = req.params + logger.log({ projectId, threadId, messageId, content }, 'editing message') + ThreadManager.findOrCreateThread(projectId, threadId, function (error, room) { + if (error) { + return next(error) + } + MessageManager.updateMessage( + room._id, + messageId, + content, + Date.now(), + function (error) { if (error) { return next(error) } res.sendStatus(204) - }) - }) - }, // No content - - editMessage(req, res, next) { - const { content } = req.body - const { projectId, threadId, messageId } = req.params - logger.log({ projectId, threadId, messageId, content }, 'editing message') - ThreadManager.findOrCreateThread( - projectId, - threadId, - function (error, room) { - if (error) { - return next(error) - } - MessageManager.updateMessage( - room._id, - messageId, - content, - Date.now(), - function (error) { - if (error) { - return next(error) - } - res.sendStatus(204) - } - ) } ) - }, - - deleteMessage(req, res, next) { - const { projectId, threadId, messageId } = req.params - logger.log({ projectId, threadId, messageId }, 'deleting message') - ThreadManager.findOrCreateThread( - projectId, - threadId, - function (error, room) { - if (error) { - return next(error) - } - MessageManager.deleteMessage( - room._id, - messageId, - function (error, message) { - if (error) { - return next(error) - } - res.sendStatus(204) - } - ) - } - ) - }, - - _sendMessage(clientThreadId, req, res, next) { - const { user_id: userId, content } = req.body - const { projectId } = req.params - if (!ObjectId.isValid(userId)) { - return res.status(400).send('Invalid userId') - } - if (!content) { - return res.status(400).send('No content provided') - } - if (content.length > this.MAX_MESSAGE_LENGTH) { - return res - .status(400) - .send(`Content too long (> ${this.MAX_MESSAGE_LENGTH} bytes)`) - } - logger.log( - { clientThreadId, projectId, userId, content }, - 'new message received' - ) - ThreadManager.findOrCreateThread( - projectId, - clientThreadId, - function (error, thread) { - if (error) { - return next(error) - } - MessageManager.createMessage( - thread._id, - userId, - content, - Date.now(), - function (error, message) { - if (error) { - return next(error) - } - message = MessageFormatter.formatMessageForClientSide(message) - message.room_id = projectId - res.status(201).send(message) - } - ) - } - ) - }, - - _getMessages(clientThreadId, req, res, next) { - let before, limit - const { projectId } = req.params - if (req.query.before) { - before = parseInt(req.query.before, 10) - } else { - before = null - } - if (req.query.limit) { - limit = parseInt(req.query.limit, 10) - } else { - limit = MessageHttpController.DEFAULT_MESSAGE_LIMIT - } - logger.log( - { limit, before, projectId, clientThreadId }, - 'get message request received' - ) - ThreadManager.findOrCreateThread( - projectId, - clientThreadId, - function (error, thread) { - if (error) { - return next(error) - } - const threadObjectId = thread._id - logger.log( - { limit, before, projectId, clientThreadId, threadObjectId }, - 'found or created thread' - ) - MessageManager.getMessages( - threadObjectId, - limit, - before, - function (error, messages) { - if (error) { - return next(error) - } - messages = MessageFormatter.formatMessagesForClientSide(messages) - logger.log({ projectId, messages }, 'got messages') - res.status(200).send(messages) - } - ) - } - ) - }, + }) +} + +function deleteMessage(req, res, next) { + const { projectId, threadId, messageId } = req.params + logger.log({ projectId, threadId, messageId }, 'deleting message') + ThreadManager.findOrCreateThread(projectId, threadId, function (error, room) { + if (error) { + return next(error) + } + MessageManager.deleteMessage( + room._id, + messageId, + function (error, message) { + if (error) { + return next(error) + } + res.sendStatus(204) + } + ) + }) +} + +function _sendMessage(clientThreadId, req, res, next) { + const { user_id: userId, content } = req.body + const { projectId } = req.params + if (!ObjectId.isValid(userId)) { + return res.status(400).send('Invalid userId') + } + if (!content) { + return res.status(400).send('No content provided') + } + if (content.length > MAX_MESSAGE_LENGTH) { + return res + .status(400) + .send(`Content too long (> ${MAX_MESSAGE_LENGTH} bytes)`) + } + logger.log( + { clientThreadId, projectId, userId, content }, + 'new message received' + ) + ThreadManager.findOrCreateThread( + projectId, + clientThreadId, + function (error, thread) { + if (error) { + return next(error) + } + MessageManager.createMessage( + thread._id, + userId, + content, + Date.now(), + function (error, message) { + if (error) { + return next(error) + } + message = MessageFormatter.formatMessageForClientSide(message) + message.room_id = projectId + res.status(201).send(message) + } + ) + } + ) +} + +function _getMessages(clientThreadId, req, res, next) { + let before, limit + const { projectId } = req.params + if (req.query.before) { + before = parseInt(req.query.before, 10) + } else { + before = null + } + if (req.query.limit) { + limit = parseInt(req.query.limit, 10) + } else { + limit = DEFAULT_MESSAGE_LIMIT + } + logger.log( + { limit, before, projectId, clientThreadId }, + 'get message request received' + ) + ThreadManager.findOrCreateThread( + projectId, + clientThreadId, + function (error, thread) { + if (error) { + return next(error) + } + const threadObjectId = thread._id + logger.log( + { limit, before, projectId, clientThreadId, threadObjectId }, + 'found or created thread' + ) + MessageManager.getMessages( + threadObjectId, + limit, + before, + function (error, messages) { + if (error) { + return next(error) + } + messages = MessageFormatter.formatMessagesForClientSide(messages) + logger.log({ projectId, messages }, 'got messages') + res.status(200).send(messages) + } + ) + } + ) +} + +module.exports = { + getGlobalMessages, + sendGlobalMessage, + sendThreadMessage, + getAllThreads, + resolveThread, + reopenThread, + deleteThread, + editMessage, + deleteMessage, } diff --git a/services/chat/app/js/Features/Messages/MessageManager.js b/services/chat/app/js/Features/Messages/MessageManager.js index c6a72b4bc5..8c6fa505c2 100644 --- a/services/chat/app/js/Features/Messages/MessageManager.js +++ b/services/chat/app/js/Features/Messages/MessageManager.js @@ -3,91 +3,94 @@ const { db, ObjectId } = require('../../mongodb') const metrics = require('@overleaf/metrics') const logger = require('@overleaf/logger') +function createMessage(roomId, userId, content, timestamp, callback) { + let newMessageOpts = { + content, + room_id: roomId, + user_id: userId, + timestamp, + } + newMessageOpts = _ensureIdsAreObjectIds(newMessageOpts) + db.messages.insertOne(newMessageOpts, function (error, confirmation) { + if (error) { + return callback(error) + } + newMessageOpts._id = confirmation.insertedId + callback(null, newMessageOpts) + }) +} + +function getMessages(roomId, limit, before, callback) { + let query = { room_id: roomId } + if (before) { + query.timestamp = { $lt: before } + } + query = _ensureIdsAreObjectIds(query) + db.messages.find(query).sort({ timestamp: -1 }).limit(limit).toArray(callback) +} + +function findAllMessagesInRooms(roomIds, callback) { + db.messages + .find({ + room_id: { $in: roomIds }, + }) + .toArray(callback) +} + +function deleteAllMessagesInRoom(roomId, callback) { + db.messages.deleteMany( + { + room_id: roomId, + }, + callback + ) +} + +function updateMessage(roomId, messageId, content, timestamp, callback) { + const query = _ensureIdsAreObjectIds({ + _id: messageId, + room_id: roomId, + }) + db.messages.updateOne( + query, + { + $set: { + content, + edited_at: timestamp, + }, + }, + callback + ) +} + +function deleteMessage(roomId, messageId, callback) { + const query = _ensureIdsAreObjectIds({ + _id: messageId, + room_id: roomId, + }) + db.messages.deleteOne(query, callback) +} + +function _ensureIdsAreObjectIds(query) { + if (query.user_id && !(query.user_id instanceof ObjectId)) { + query.user_id = ObjectId(query.user_id) + } + if (query.room_id && !(query.room_id instanceof ObjectId)) { + query.room_id = ObjectId(query.room_id) + } + if (query._id && !(query._id instanceof ObjectId)) { + query._id = ObjectId(query._id) + } + return query +} + module.exports = MessageManager = { - createMessage(roomId, userId, content, timestamp, callback) { - let newMessageOpts = { - content, - room_id: roomId, - user_id: userId, - timestamp, - } - newMessageOpts = this._ensureIdsAreObjectIds(newMessageOpts) - db.messages.insertOne(newMessageOpts, function (error, confirmation) { - if (error) { - return callback(error) - } - newMessageOpts._id = confirmation.insertedId - callback(null, newMessageOpts) - }) - }, - - getMessages(roomId, limit, before, callback) { - let query = { room_id: roomId } - if (before) { - query.timestamp = { $lt: before } - } - query = this._ensureIdsAreObjectIds(query) - db.messages - .find(query) - .sort({ timestamp: -1 }) - .limit(limit) - .toArray(callback) - }, - - findAllMessagesInRooms(roomIds, callback) { - db.messages - .find({ - room_id: { $in: roomIds }, - }) - .toArray(callback) - }, - - deleteAllMessagesInRoom(roomId, callback) { - db.messages.deleteMany( - { - room_id: roomId, - }, - callback - ) - }, - - updateMessage(roomId, messageId, content, timestamp, callback) { - const query = this._ensureIdsAreObjectIds({ - _id: messageId, - room_id: roomId, - }) - db.messages.updateOne( - query, - { - $set: { - content, - edited_at: timestamp, - }, - }, - callback - ) - }, - - deleteMessage(roomId, messageId, callback) { - const query = this._ensureIdsAreObjectIds({ - _id: messageId, - room_id: roomId, - }) - db.messages.deleteOne(query, callback) - }, - - _ensureIdsAreObjectIds(query) { - if (query.user_id && !(query.user_id instanceof ObjectId)) { - query.user_id = ObjectId(query.user_id) - } - if (query.room_id && !(query.room_id instanceof ObjectId)) { - query.room_id = ObjectId(query.room_id) - } - if (query._id && !(query._id instanceof ObjectId)) { - query._id = ObjectId(query._id) - } - return query - }, + createMessage, + getMessages, + findAllMessagesInRooms, + deleteAllMessagesInRoom, + updateMessage, + deleteMessage, } ;[ 'createMessage', diff --git a/services/chat/app/js/Features/Threads/ThreadManager.js b/services/chat/app/js/Features/Threads/ThreadManager.js index e9089c63c7..4d9bd8da3d 100644 --- a/services/chat/app/js/Features/Threads/ThreadManager.js +++ b/services/chat/app/js/Features/Threads/ThreadManager.js @@ -3,114 +3,120 @@ const { db, ObjectId } = require('../../mongodb') const logger = require('@overleaf/logger') const metrics = require('@overleaf/metrics') -module.exports = ThreadManager = { - GLOBAL_THREAD: 'GLOBAL', +const GLOBAL_THREAD = 'GLOBAL' - findOrCreateThread(projectId, threadId, callback) { - let query, update - projectId = ObjectId(projectId.toString()) - if (threadId !== ThreadManager.GLOBAL_THREAD) { - threadId = ObjectId(threadId.toString()) +function findOrCreateThread(projectId, threadId, callback) { + let query, update + projectId = ObjectId(projectId.toString()) + if (threadId !== GLOBAL_THREAD) { + threadId = ObjectId(threadId.toString()) + } + + if (threadId === GLOBAL_THREAD) { + query = { + project_id: projectId, + thread_id: { $exists: false }, } - - if (threadId === ThreadManager.GLOBAL_THREAD) { - query = { - project_id: projectId, - thread_id: { $exists: false }, - } - update = { - project_id: projectId, - } - } else { - query = { - project_id: projectId, - thread_id: threadId, - } - update = { - project_id: projectId, - thread_id: threadId, - } + update = { + project_id: projectId, } + } else { + query = { + project_id: projectId, + thread_id: threadId, + } + update = { + project_id: projectId, + thread_id: threadId, + } + } - db.rooms.findOneAndUpdate( - query, - { $set: update }, - { upsert: true, returnDocument: 'after' }, - function (error, result) { - if (error) { - return callback(error) - } - callback(null, result.value) - } - ) - }, - - findAllThreadRooms(projectId, callback) { - db.rooms - .find( - { - project_id: ObjectId(projectId.toString()), - thread_id: { $exists: true }, - }, - { - thread_id: 1, - resolved: 1, - } - ) - .toArray(callback) - }, - - resolveThread(projectId, threadId, userId, callback) { - db.rooms.updateOne( - { - project_id: ObjectId(projectId.toString()), - thread_id: ObjectId(threadId.toString()), - }, - { - $set: { - resolved: { - user_id: userId, - ts: new Date(), - }, - }, - }, - callback - ) - }, - - reopenThread(projectId, threadId, callback) { - db.rooms.updateOne( - { - project_id: ObjectId(projectId.toString()), - thread_id: ObjectId(threadId.toString()), - }, - { - $unset: { - resolved: true, - }, - }, - callback - ) - }, - - deleteThread(projectId, threadId, callback) { - this.findOrCreateThread(projectId, threadId, function (error, room) { + db.rooms.findOneAndUpdate( + query, + { $set: update }, + { upsert: true, returnDocument: 'after' }, + function (error, result) { if (error) { return callback(error) } - db.rooms.deleteOne( - { - _id: room._id, + callback(null, result.value) + } + ) +} + +function findAllThreadRooms(projectId, callback) { + db.rooms + .find( + { + project_id: ObjectId(projectId.toString()), + thread_id: { $exists: true }, + }, + { + thread_id: 1, + resolved: 1, + } + ) + .toArray(callback) +} + +function resolveThread(projectId, threadId, userId, callback) { + db.rooms.updateOne( + { + project_id: ObjectId(projectId.toString()), + thread_id: ObjectId(threadId.toString()), + }, + { + $set: { + resolved: { + user_id: userId, + ts: new Date(), }, - function (error) { - if (error) { - return callback(error) - } - callback(null, room._id) + }, + }, + callback + ) +} + +function reopenThread(projectId, threadId, callback) { + db.rooms.updateOne( + { + project_id: ObjectId(projectId.toString()), + thread_id: ObjectId(threadId.toString()), + }, + { + $unset: { + resolved: true, + }, + } + ) +} + +function deleteThread(projectId, threadId, callback) { + findOrCreateThread(projectId, threadId, function (error, room) { + if (error) { + return callback(error) + } + db.rooms.deleteOne( + { + _id: room._id, + }, + function (error) { + if (error) { + return callback(error) } - ) - }) - }, + callback(null, room._id) + } + ) + }) +} + +module.exports = ThreadManager = { + GLOBAL_THREAD, + findOrCreateThread, + findAllThreadRooms, + resolveThread, + reopenThread, + deleteThread, } ;[ 'findOrCreateThread',