Messenger/packages/backend/src/modules/messages/messages.routes.ts
Заид Омар Медхат | Zaid Omar Medhat db5c1610b3 Phase 6: media messages + avatars via MinIO presigned uploads
- Media (voice/video/image/file): private bucket, presigned PUT upload,
  reference-only publish, membership-gated presigned GET for viewing
- Avatars: public bucket, presigned PUT + direct public URL, profile.update event
- messages.media_key + media_meta (migration 1720000000005_media)
- Explicit MinIO region on presigning client (avoids getBucketRegion network call)
- Event type values sourced from EventType constants (no raw string literals)
- Web: attach button, MediaView renderer, avatar upload in topbar

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BFRyKxkKEjfAgpXygzoNiD
2026-07-10 18:11:25 +05:00

211 lines
6.8 KiB
TypeScript

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<void> => {
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();
};