import type { FastifyInstance } from 'fastify'; import { sendMessageBodySchema, editMessageBodySchema, reactionBodySchema, messageSchema, messageListSchema, mediaUrlSchema, errorSchema, } from '@altricade/core'; import type { SendMessageBody, EditMessageBody, ReactionBody } from '@altricade/core'; const bearerAuth = [{ bearerAuth: [] }]; const idParamsSchema = { type: 'object', required: ['id'], properties: { id: { type: 'string', format: 'uuid' } }, } as const; const messageParamsSchema = { type: 'object', required: ['id', 'messageId'], properties: { id: { type: 'string', format: 'uuid' }, messageId: { type: 'string', format: 'uuid' }, }, } as const; const reactionParamsSchema = { type: 'object', required: ['id', 'messageId', 'emoji'], properties: { id: { type: 'string', format: 'uuid' }, messageId: { type: 'string', format: 'uuid' }, emoji: { type: 'string', minLength: 1, maxLength: 32 }, }, } as const; const historyQuerySchema = { type: 'object', properties: { before: { type: 'integer', minimum: 1 }, limit: { type: 'integer', minimum: 1, maximum: 100, default: 50 }, }, } as const; export const messagesRoutes = (app: FastifyInstance): Promise => { app.get<{ Params: { id: string }; Querystring: { before?: number; limit: number } }>( '/conversations/:id/messages', { schema: { tags: ['messages'], summary: 'Load conversation message history (newest first)', security: bearerAuth, params: idParamsSchema, querystring: historyQuerySchema, response: { 200: messageListSchema, 401: errorSchema, 403: errorSchema }, }, preHandler: app.authenticate, }, async (request, reply) => { const user = request.authUser; if (user === undefined) return reply.code(401).send({ error: 'unauthorized' }); const before = request.query.before ?? null; const history = await app.messagesService.history( request.params.id, user.id, before, request.query.limit, ); return reply.send(history); }, ); app.post<{ Params: { id: string }; Body: SendMessageBody }>( '/conversations/:id/messages', { schema: { tags: ['messages'], summary: 'Send a message to a conversation', security: bearerAuth, params: idParamsSchema, body: sendMessageBodySchema, response: { 200: messageSchema, 201: messageSchema, 401: errorSchema, 403: errorSchema }, }, preHandler: app.authenticate, }, async (request, reply) => { const user = request.authUser; if (user === undefined) return reply.code(401).send({ error: 'unauthorized' }); const result = await app.messagesService.send(request.params.id, user.id, request.body); return reply.code(result.created ? 201 : 200).send(result.message); }, ); app.patch<{ Params: { id: string; messageId: string }; Body: EditMessageBody }>( '/conversations/:id/messages/:messageId', { schema: { tags: ['messages'], summary: 'Edit a message (sender only)', security: bearerAuth, params: messageParamsSchema, body: editMessageBodySchema, response: { 200: messageSchema, 401: errorSchema, 403: errorSchema, 404: errorSchema }, }, preHandler: app.authenticate, }, async (request, reply) => { const user = request.authUser; if (user === undefined) return reply.code(401).send({ error: 'unauthorized' }); const message = await app.messagesService.edit( request.params.id, request.params.messageId, user.id, request.body.content, ); return reply.send(message); }, ); app.delete<{ Params: { id: string; messageId: string } }>( '/conversations/:id/messages/:messageId', { schema: { tags: ['messages'], summary: 'Delete a message (sender only, soft delete)', security: bearerAuth, params: messageParamsSchema, response: { 401: errorSchema, 403: errorSchema, 404: errorSchema }, }, preHandler: app.authenticate, }, async (request, reply) => { const user = request.authUser; if (user === undefined) return reply.code(401).send({ error: 'unauthorized' }); await app.messagesService.remove(request.params.id, request.params.messageId, user.id); return reply.code(204).send(); }, ); app.post<{ Params: { id: string; messageId: string }; Body: ReactionBody }>( '/conversations/:id/messages/:messageId/reactions', { schema: { tags: ['messages'], summary: 'Add a reaction', security: bearerAuth, params: messageParamsSchema, body: reactionBodySchema, response: { 401: errorSchema, 403: errorSchema, 404: errorSchema }, }, preHandler: app.authenticate, }, async (request, reply) => { const user = request.authUser; if (user === undefined) return reply.code(401).send({ error: 'unauthorized' }); await app.messagesService.addReaction( request.params.id, request.params.messageId, user.id, request.body.emoji, ); return reply.code(204).send(); }, ); app.get<{ Params: { id: string; messageId: string } }>( '/conversations/:id/messages/:messageId/media-url', { schema: { tags: ['media'], summary: 'Get a presigned download URL for a message attachment (member only)', security: bearerAuth, params: messageParamsSchema, response: { 200: mediaUrlSchema, 401: errorSchema, 403: errorSchema, 404: errorSchema }, }, preHandler: app.authenticate, }, async (request, reply) => { const user = request.authUser; if (user === undefined) return reply.code(401).send({ error: 'unauthorized' }); const url = await app.messagesService.mediaUrl(request.params.id, request.params.messageId, user.id); return reply.send({ url }); }, ); app.delete<{ Params: { id: string; messageId: string; emoji: string } }>( '/conversations/:id/messages/:messageId/reactions/:emoji', { schema: { tags: ['messages'], summary: 'Remove a reaction', security: bearerAuth, params: reactionParamsSchema, response: { 401: errorSchema, 403: errorSchema, 404: errorSchema }, }, preHandler: app.authenticate, }, async (request, reply) => { const user = request.authUser; if (user === undefined) return reply.code(401).send({ error: 'unauthorized' }); await app.messagesService.removeReaction( request.params.id, request.params.messageId, user.id, decodeURIComponent(request.params.emoji), ); return reply.code(204).send(); }, ); return Promise.resolve(); };