MessageHttpController.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369
  1. import logger from '@overleaf/logger'
  2. import * as MessageManager from './MessageManager.js'
  3. import * as MessageFormatter from './MessageFormatter.js'
  4. import * as ThreadManager from '../Threads/ThreadManager.js'
  5. import { ObjectId } from '../../mongodb.js'
  6. const DEFAULT_MESSAGE_LIMIT = 50
  7. const MAX_MESSAGE_LENGTH = 10 * 1024 // 10kb, about 1,500 words
  8. function readContext(context, req) {
  9. req.body = context.requestBody
  10. req.params = context.params.path
  11. req.query = context.params.query
  12. if (typeof req.params.projectId !== 'undefined') {
  13. if (!ObjectId.isValid(req.params.projectId)) {
  14. context.res.status(400).setBody('Invalid projectId')
  15. }
  16. }
  17. if (typeof req.params.threadId !== 'undefined') {
  18. if (!ObjectId.isValid(req.params.threadId)) {
  19. context.res.status(400).setBody('Invalid threadId')
  20. }
  21. }
  22. }
  23. /**
  24. * @param context
  25. * @param {(req: unknown, res: unknown) => Promise<unknown>} ControllerMethod
  26. * @returns {Promise<*>}
  27. */
  28. export async function callMessageHttpController(context, ControllerMethod) {
  29. const req = {}
  30. readContext(context, req)
  31. if (context.res.statusCode !== 400) {
  32. return await ControllerMethod(req, context.res)
  33. } else {
  34. return context.res.body
  35. }
  36. }
  37. export async function getGlobalMessages(context) {
  38. return await callMessageHttpController(context, _getGlobalMessages)
  39. }
  40. export async function getGlobalMessage(context) {
  41. return await callMessageHttpController(context, _getGlobalMessage)
  42. }
  43. export async function sendGlobalMessage(context) {
  44. return await callMessageHttpController(context, _sendGlobalMessage)
  45. }
  46. export async function sendMessage(context) {
  47. return await callMessageHttpController(context, _sendThreadMessage)
  48. }
  49. export async function getThreads(context) {
  50. return await callMessageHttpController(context, _getAllThreads)
  51. }
  52. export async function getThread(context) {
  53. return await callMessageHttpController(context, _getThread)
  54. }
  55. export async function resolveThread(context) {
  56. return await callMessageHttpController(context, _resolveThread)
  57. }
  58. export async function reopenThread(context) {
  59. return await callMessageHttpController(context, _reopenThread)
  60. }
  61. export async function deleteThread(context) {
  62. return await callMessageHttpController(context, _deleteThread)
  63. }
  64. export async function editMessage(context) {
  65. return await callMessageHttpController(context, _editMessage)
  66. }
  67. export async function deleteMessage(context) {
  68. return await callMessageHttpController(context, _deleteMessage)
  69. }
  70. export async function deleteUserMessage(context) {
  71. return await callMessageHttpController(context, _deleteUserMessage)
  72. }
  73. export async function getResolvedThreadIds(context) {
  74. return await callMessageHttpController(context, _getResolvedThreadIds)
  75. }
  76. export async function destroyProject(context) {
  77. return await callMessageHttpController(context, _destroyProject)
  78. }
  79. export async function duplicateCommentThreads(context) {
  80. return await callMessageHttpController(context, _duplicateCommentThreads)
  81. }
  82. export async function generateThreadData(context) {
  83. return await callMessageHttpController(context, _generateThreadData)
  84. }
  85. export async function getStatus(context) {
  86. const message = 'chat is alive'
  87. context.res.status(200).setBody(message)
  88. return message
  89. }
  90. const _getGlobalMessages = async (req, res) => {
  91. await _getMessages(ThreadManager.GLOBAL_THREAD, req, res)
  92. }
  93. const _getGlobalMessage = async (req, res) => {
  94. const { projectId, messageId } = req.params
  95. logger.debug({ projectId, messageId }, 'getting single global message')
  96. try {
  97. const room = await ThreadManager.findThread(
  98. projectId,
  99. ThreadManager.GLOBAL_THREAD
  100. )
  101. const message = await MessageManager.getMessage(room._id, messageId)
  102. const formattedMsg = MessageFormatter.formatMessageForClientSide(message)
  103. res.status(200).setBody(formattedMsg)
  104. } catch (error) {
  105. if (
  106. error instanceof ThreadManager.MissingThreadError ||
  107. error instanceof MessageManager.MissingMessageError
  108. ) {
  109. res.status(404)
  110. return
  111. }
  112. throw error
  113. }
  114. }
  115. async function _sendGlobalMessage(req, res) {
  116. const { user_id: userId, content } = req.body
  117. const { projectId } = req.params
  118. return await _sendMessage(
  119. userId,
  120. projectId,
  121. content,
  122. ThreadManager.GLOBAL_THREAD,
  123. res
  124. )
  125. }
  126. async function _sendThreadMessage(req, res) {
  127. const { user_id: userId, content } = req.body
  128. const { projectId, threadId } = req.params
  129. return await _sendMessage(userId, projectId, content, threadId, res)
  130. }
  131. const _getAllThreads = async (req, res) => {
  132. const { projectId } = req.params
  133. logger.debug({ projectId }, 'getting all threads')
  134. const rooms = await ThreadManager.findAllThreadRooms(projectId)
  135. const roomIds = rooms.map(r => r._id)
  136. const messages = await MessageManager.findAllMessagesInRooms(roomIds)
  137. const threads = MessageFormatter.groupMessagesByThreads(rooms, messages)
  138. res.json(threads)
  139. }
  140. const _generateThreadData = async (req, res) => {
  141. const { projectId } = req.params
  142. const { threads } = req.body
  143. logger.debug({ projectId }, 'getting all threads')
  144. const rooms = await ThreadManager.findThreadsById(projectId, threads)
  145. const roomIds = rooms.map(r => r._id)
  146. const messages = await MessageManager.findAllMessagesInRooms(roomIds)
  147. logger.debug({ rooms, messages }, 'looked up messages in the rooms')
  148. const threadData = MessageFormatter.groupMessagesByThreads(rooms, messages)
  149. res.json(threadData)
  150. }
  151. const _getThread = async (req, res) => {
  152. const { projectId, threadId } = req.params
  153. logger.debug({ projectId, threadId }, 'getting specific thread')
  154. try {
  155. const room = await ThreadManager.findThread(projectId, threadId)
  156. const messages = await MessageManager.findAllMessagesInRooms([room._id])
  157. const threads = MessageFormatter.groupMessagesByThreads([room], messages)
  158. const thread = threads[threadId] || null
  159. if (!thread) {
  160. res.status(404)
  161. return
  162. }
  163. res.json(thread)
  164. } catch (error) {
  165. if (error instanceof ThreadManager.MissingThreadError) {
  166. res.status(404)
  167. return
  168. }
  169. throw error
  170. }
  171. }
  172. const _resolveThread = async (req, res) => {
  173. const { projectId, threadId } = req.params
  174. const { user_id: userId } = req.body
  175. logger.debug({ userId, projectId, threadId }, 'marking thread as resolved')
  176. await ThreadManager.resolveThread(projectId, threadId, userId)
  177. res.status(204)
  178. }
  179. const _reopenThread = async (req, res) => {
  180. const { projectId, threadId } = req.params
  181. logger.debug({ projectId, threadId }, 'reopening thread')
  182. await ThreadManager.reopenThread(projectId, threadId)
  183. res.status(204)
  184. }
  185. const _deleteThread = async (req, res) => {
  186. const { projectId, threadId } = req.params
  187. logger.debug({ projectId, threadId }, 'deleting thread')
  188. const roomId = await ThreadManager.deleteThread(projectId, threadId)
  189. await MessageManager.deleteAllMessagesInRoom(roomId)
  190. res.status(204)
  191. }
  192. const _editMessage = async (req, res) => {
  193. const { content, userId } = req.body
  194. const { projectId, threadId, messageId } = req.params
  195. logger.debug({ projectId, threadId, messageId, content }, 'editing message')
  196. const room = await ThreadManager.findOrCreateThread(projectId, threadId)
  197. const found = await MessageManager.updateMessage(
  198. room._id,
  199. messageId,
  200. userId,
  201. content,
  202. Date.now()
  203. )
  204. if (!found) {
  205. res.status(404)
  206. return
  207. }
  208. res.status(204)
  209. }
  210. const _deleteMessage = async (req, res) => {
  211. const { projectId, threadId, messageId } = req.params
  212. logger.debug({ projectId, threadId, messageId }, 'deleting message')
  213. const room = await ThreadManager.findOrCreateThread(projectId, threadId)
  214. await MessageManager.deleteMessage(room._id, messageId)
  215. res.status(204)
  216. }
  217. const _deleteUserMessage = async (req, res) => {
  218. const { projectId, threadId, userId, messageId } = req.params
  219. const room = await ThreadManager.findOrCreateThread(projectId, threadId)
  220. await MessageManager.deleteUserMessage(userId, room._id, messageId)
  221. res.status(204)
  222. }
  223. const _getResolvedThreadIds = async (req, res) => {
  224. const { projectId } = req.params
  225. const resolvedThreadIds = await ThreadManager.getResolvedThreadIds(projectId)
  226. res.json({ resolvedThreadIds })
  227. }
  228. const _destroyProject = async (req, res) => {
  229. const { projectId } = req.params
  230. logger.debug({ projectId }, 'destroying project')
  231. const rooms = await ThreadManager.findAllThreadRoomsAndGlobalThread(projectId)
  232. const roomIds = rooms.map(r => r._id)
  233. logger.debug({ projectId, roomIds }, 'deleting all messages in rooms')
  234. await MessageManager.deleteAllMessagesInRooms(roomIds)
  235. logger.debug({ projectId }, 'deleting all threads in project')
  236. await ThreadManager.deleteAllThreadsInProject(projectId)
  237. res.status(204)
  238. }
  239. async function _sendMessage(userId, projectId, content, clientThreadId, res) {
  240. if (!ObjectId.isValid(userId)) {
  241. const message = 'Invalid userId'
  242. res.status(400).setBody(message)
  243. return message
  244. }
  245. if (!content) {
  246. const message = 'No content provided'
  247. res.status(400).setBody(message)
  248. return message
  249. }
  250. if (content.length > MAX_MESSAGE_LENGTH) {
  251. const message = `Content too long (> ${MAX_MESSAGE_LENGTH} bytes)`
  252. res.status(400).setBody(message)
  253. return message
  254. }
  255. logger.debug(
  256. { clientThreadId, projectId, userId, content },
  257. 'new message received'
  258. )
  259. const thread = await ThreadManager.findOrCreateThread(
  260. projectId,
  261. clientThreadId
  262. )
  263. let message = await MessageManager.createMessage(
  264. thread._id,
  265. userId,
  266. content,
  267. Date.now()
  268. )
  269. message = MessageFormatter.formatMessageForClientSide(message)
  270. message.room_id = projectId
  271. res.status(201).setBody(message)
  272. }
  273. async function _getMessages(clientThreadId, req, res) {
  274. let before, limit
  275. const { projectId } = req.params
  276. if (req.query.before) {
  277. before = parseInt(req.query.before, 10)
  278. } else {
  279. before = null
  280. }
  281. if (req.query.limit) {
  282. limit = parseInt(req.query.limit, 10)
  283. } else {
  284. limit = DEFAULT_MESSAGE_LIMIT
  285. }
  286. logger.debug(
  287. { limit, before, projectId, clientThreadId },
  288. 'get message request received'
  289. )
  290. const thread = await ThreadManager.findOrCreateThread(
  291. projectId,
  292. clientThreadId
  293. )
  294. const threadObjectId = thread._id
  295. logger.debug(
  296. { limit, before, projectId, clientThreadId, threadObjectId },
  297. 'found or created thread'
  298. )
  299. let messages = await MessageManager.getMessages(threadObjectId, limit, before)
  300. messages = MessageFormatter.formatMessagesForClientSide(messages)
  301. logger.debug({ projectId, messages }, 'got messages')
  302. res.status(200).setBody(messages)
  303. }
  304. async function _duplicateCommentThreads(req, res) {
  305. const { projectId } = req.params
  306. const { threads } = req.body
  307. const result = {}
  308. for (const id of threads) {
  309. logger.debug({ projectId, thread: id }, 'duplicating thread')
  310. try {
  311. const { oldRoom, newRoom } = await ThreadManager.duplicateThread(
  312. projectId,
  313. id
  314. )
  315. await MessageManager.duplicateRoomToOtherRoom(oldRoom._id, newRoom._id)
  316. result[id] = { duplicateId: newRoom.thread_id }
  317. } catch (error) {
  318. if (error instanceof ThreadManager.MissingThreadError) {
  319. // Expected error when the comment has been deleted prior to duplication
  320. result[id] = { error: 'not found' }
  321. } else {
  322. logger.err({ error }, 'error duplicating thread')
  323. result[id] = { error: 'unknown' }
  324. }
  325. }
  326. }
  327. res.json({ newThreads: result })
  328. }