From 423144ea61bfd7a71019aa93f435314e93a647f3 Mon Sep 17 00:00:00 2001 From: Domagoj Kriskovic Date: Thu, 4 Sep 2025 15:24:26 +0200 Subject: [PATCH] [dsmp] Add endpoint to retrieve a single message from chat (#28242) * [dsmp] Add endpoint to retrieve a single message from chat * use user:null in case is deleted GitOrigin-RevId: f42360c2e05cfe93fa11230ac3cc311bdb044c1d --- .../Messages/MessageHttpController.js | 29 +++++++++++++++++++ .../js/Features/Messages/MessageManager.js | 14 +++++++++ .../app/js/Features/Threads/ThreadManager.js | 9 ++++-- services/chat/chat.yaml | 26 +++++++++++++++++ .../app/src/Features/Chat/ChatApiHandler.js | 8 +++++ .../test/acceptance/src/mocks/MockChatApi.mjs | 10 +++++++ 6 files changed, 94 insertions(+), 2 deletions(-) diff --git a/services/chat/app/js/Features/Messages/MessageHttpController.js b/services/chat/app/js/Features/Messages/MessageHttpController.js index fc587e4549..13e8f3ab61 100644 --- a/services/chat/app/js/Features/Messages/MessageHttpController.js +++ b/services/chat/app/js/Features/Messages/MessageHttpController.js @@ -42,6 +42,10 @@ export async function getGlobalMessages(context) { return await callMessageHttpController(context, _getGlobalMessages) } +export async function getGlobalMessage(context) { + return await callMessageHttpController(context, _getGlobalMessage) +} + export async function sendGlobalMessage(context) { return await callMessageHttpController(context, _sendGlobalMessage) } @@ -108,6 +112,31 @@ const _getGlobalMessages = async (req, res) => { await _getMessages(ThreadManager.GLOBAL_THREAD, req, res) } +const _getGlobalMessage = async (req, res) => { + const { projectId, messageId } = req.params + logger.debug({ projectId, messageId }, 'getting single global message') + try { + const room = await ThreadManager.findThread( + projectId, + ThreadManager.GLOBAL_THREAD + ) + + const message = await MessageManager.getMessage(room._id, messageId) + const formattedMsg = MessageFormatter.formatMessageForClientSide(message) + + res.status(200).setBody(formattedMsg) + } catch (error) { + if ( + error instanceof ThreadManager.MissingThreadError || + error instanceof MessageManager.MissingMessageError + ) { + res.status(404) + return + } + throw error + } +} + async function _sendGlobalMessage(req, res) { const { user_id: userId, content } = req.body const { projectId } = req.params diff --git a/services/chat/app/js/Features/Messages/MessageManager.js b/services/chat/app/js/Features/Messages/MessageManager.js index efff22a2a4..f020d71149 100644 --- a/services/chat/app/js/Features/Messages/MessageManager.js +++ b/services/chat/app/js/Features/Messages/MessageManager.js @@ -1,5 +1,7 @@ import { db, ObjectId } from '../../mongodb.js' +export class MissingMessageError extends Error {} + export async function createMessage(roomId, userId, content, timestamp) { let newMessageOpts = { content, @@ -85,6 +87,18 @@ export async function deleteUserMessage(userId, roomId, messageId) { }) } +export async function getMessage(roomId, messageId) { + const query = _ensureIdsAreObjectIds({ + _id: messageId, + room_id: roomId, + }) + const message = await db.messages.findOne(query) + if (!message) { + throw new MissingMessageError(`Message not found`) + } + return message +} + function _ensureIdsAreObjectIds(query) { if (query.user_id && !(query.user_id instanceof ObjectId)) { query.user_id = new ObjectId(query.user_id) diff --git a/services/chat/app/js/Features/Threads/ThreadManager.js b/services/chat/app/js/Features/Threads/ThreadManager.js index 9cab1e2f72..5ad05b8690 100644 --- a/services/chat/app/js/Features/Threads/ThreadManager.js +++ b/services/chat/app/js/Features/Threads/ThreadManager.js @@ -148,9 +148,14 @@ export async function duplicateThread(projectId, threadId) { } export async function findThread(projectId, threadId) { + projectId = new ObjectId(projectId.toString()) + if (threadId !== GLOBAL_THREAD) { + threadId = new ObjectId(threadId.toString()) + } + const room = await db.rooms.findOne({ - project_id: new ObjectId(projectId), - thread_id: new ObjectId(threadId), + project_id: projectId, + thread_id: threadId === GLOBAL_THREAD ? { $exists: false } : threadId, }) if (!room) { throw new MissingThreadError('Thread not found') diff --git a/services/chat/chat.yaml b/services/chat/chat.yaml index ce07d84a04..455921a0b4 100644 --- a/services/chat/chat.yaml +++ b/services/chat/chat.yaml @@ -56,6 +56,32 @@ paths: content: string description: 'UserID and Content of the message to be posted. ' description: Send global message for the project with Project ID provided + '/project/{projectId}/messages/{messageId}': + parameters: + - schema: + type: string + name: projectId + in: path + required: true + - schema: + type: string + name: messageId + in: path + required: true + get: + summary: Get single global message + tags: [] + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Message' + '404': + description: Message not found + operationId: getGlobalMessage + description: Get a single global message by message ID for the project with Project ID provided '/project/{projectId}/thread/{threadId}/messages': parameters: - schema: diff --git a/services/web/app/src/Features/Chat/ChatApiHandler.js b/services/web/app/src/Features/Chat/ChatApiHandler.js index 7d03e7eb08..7a4f4df31c 100644 --- a/services/web/app/src/Features/Chat/ChatApiHandler.js +++ b/services/web/app/src/Features/Chat/ChatApiHandler.js @@ -39,6 +39,12 @@ async function getGlobalMessages(projectId, limit, before) { return await fetchJson(url) } +async function getGlobalMessage(projectId, messageId) { + return await fetchJson( + chatApiUrl(`/project/${projectId}/messages/${messageId}`) + ) +} + async function sendComment(projectId, threadId, userId, content) { const comment = await fetchJson( chatApiUrl(`/project/${projectId}/thread/${threadId}/messages`), @@ -142,6 +148,7 @@ module.exports = { destroyProject: callbackify(destroyProject), sendGlobalMessage: callbackify(sendGlobalMessage), getGlobalMessages: callbackify(getGlobalMessages), + getGlobalMessage: callbackify(getGlobalMessage), sendComment: callbackify(sendComment), resolveThread: callbackify(resolveThread), reopenThread: callbackify(reopenThread), @@ -158,6 +165,7 @@ module.exports = { destroyProject, sendGlobalMessage, getGlobalMessages, + getGlobalMessage, sendComment, resolveThread, reopenThread, diff --git a/services/web/test/acceptance/src/mocks/MockChatApi.mjs b/services/web/test/acceptance/src/mocks/MockChatApi.mjs index 0dc056a637..e3fe94d631 100644 --- a/services/web/test/acceptance/src/mocks/MockChatApi.mjs +++ b/services/web/test/acceptance/src/mocks/MockChatApi.mjs @@ -42,6 +42,16 @@ class MockChatApi extends AbstractMockApi { this.app.post('/project/:project_id/messages', (req, res) => { res.json(this.sendMessage(req.params.project_id, 'global', req.body)) }) + this.app.get('/project/:project_id/messages/:message_id', (req, res) => { + const projectId = req.params.project_id + const messageId = req.params.message_id + const thread = this.getThread(projectId, 'global') + const message = thread.find(msg => msg.id === messageId) + if (!message) { + return res.status(404).json({ error: 'Message not found' }) + } + res.json(message) + }) this.app.get( '/project/:project_id/thread/:thread_id/messages', (req, res) => {