Módulo Internal Chat
src/app/modules/internal-chat/
Mensajería interna entre agentes y administradores del mismo tenant: conversaciones directas (1:1) y grupos, con mensajes de texto o archivo, menciones, lectura, presencia y mensajes de sistema para cambios de membresía. Es independiente del chat con contactos (ver Chat); ambos comparten el namespace de Socket.IO, por eso los eventos internos llevan el prefijo im:.
Modelos
Conversation (colección internal_conversations)
| Campo | Tipo | Descripción |
|---|---|---|
tenantId | ObjectId → Tenant | Tenant |
type | enum | direct o group |
name / description | string | Solo grupos |
photoFileId | ObjectId → File | Foto del grupo |
createdBy | ObjectId → User | Creador |
directKey | string | Para direct: los dos ids ordenados y unidos con : |
lastMessageAt, lastMessagePreview, lastMessageSenderId | Date, string, ObjectId | Resumen del último mensaje |
Índices: (tenantId, lastMessageAt desc) y único parcial (tenantId, directKey) solo para conversaciones direct, que evita duplicar un 1:1 aun con peticiones concurrentes.
ConversationMember (colección internal_conversation_members, sin timestamps)
| Campo | Tipo | Descripción |
|---|---|---|
tenantId, conversationId, userId | ObjectId | Relación |
role | enum | admin o member (por defecto member) |
lastReadAt | Date | Última lectura del usuario |
joinedAt | Date | Fecha de ingreso |
Índice único (conversationId, userId).
InternalMessage (colección internal_messages)
| Campo | Tipo | Descripción |
|---|---|---|
tenantId, conversationId | ObjectId | Relación |
senderId | ObjectId → User | Ausente en mensajes de sistema |
content | string | Texto |
contentType | enum | text (por defecto), file o system |
fileId | ObjectId → File | Adjunto |
mentions | ObjectId[] → User | Usuarios mencionados |
systemEvent | subdocumento | type (member_added, member_removed, left), actorId, targetIds |
Índice (conversationId, createdAt desc).
Endpoints
Base: /api/v1/internal-conversations. Controlador con @Auth() y PermissionsGuard. Solo la creación y edición de grupos declaran permiso; el resto exige únicamente ser miembro de la conversación (un no miembro recibe 404, igual que una conversación inexistente). Todo se limita al tenant del usuario.
| Método | Ruta | Permiso | Descripción |
|---|---|---|---|
POST | /internal-conversations/direct | Solo sesión | Crea (o recupera) la conversación 1:1 con otro usuario |
POST | /internal-conversations/group | INTERNAL_CHAT_CREATE | Crea un grupo (el servicio exige userType = admin) |
GET | /internal-conversations | Solo sesión | Mis conversaciones, paginadas |
GET | /internal-conversations/agents | Solo sesión | Agentes y administradores del tenant, con presencia |
GET | /internal-conversations/:id | Solo sesión | Detalle de una conversación (data.results con un elemento) |
PATCH | /internal-conversations/:id | INTERNAL_CHAT_UPDATE | Edita nombre, descripción o foto de un grupo |
GET | /internal-conversations/:id/messages | Solo sesión | Mensajes, paginados |
POST | /internal-conversations/:id/messages | Solo sesión | Envía un mensaje |
GET | /internal-conversations/:id/messages/:messageId/window | Solo sesión | Ventana de mensajes alrededor de uno (para saltar a un destacado) |
GET | /internal-conversations/:id/members | Solo sesión | Miembros con datos de usuario y presencia |
POST | /internal-conversations/:id/members | Solo sesión (el servicio exige administrador) | Agrega miembros a un grupo |
DELETE | /internal-conversations/:id/members/:userId | Solo sesión (el servicio exige administrador) | Quita un miembro |
POST | /internal-conversations/:id/leave | Solo sesión | Abandona un grupo |
POST | /internal-conversations/:id/read | Solo sesión | Marca la conversación como leída |
Los listados devuelven data.results y data.meta { total, page, limit } (por defecto page = 1, limit = 20).
Reglas de autorización en los servicios
- Crear grupo: solo usuarios con
userType = admin; quien lo crea queda comoadmindel grupo. - Agregar o quitar miembros y editar grupos: un administrador del tenant (
userType = admin) o un miembro con roladmindel grupo. En otro caso,403. - Un administrador del grupo que no es administrador del tenant no puede dejar el grupo sin administradores (
400,lastAdminCannotBeRemoved). - Una conversación
directno se puede abandonar (400). - Los usuarios agregados deben pertenecer al tenant.
DTOs
Validaciones de class-validator. "Opcional" indica @IsOptional().
Parámetros de ruta
| Parámetro | Tipo | Obligatorio | Validación | Usado en |
|---|---|---|---|---|
id | string | Sí | Ninguna en el controlador | Todas las rutas con :id |
messageId | string | Sí | Ninguna | :id/messages/:messageId/window |
userId | string | Sí | Ninguna | DELETE :id/members/:userId |
CreateDirectConversationDto (body de POST /direct)
| Campo | Tipo | Obligatorio | Validaciones | Descripción |
|---|---|---|---|---|
targetUserId | string | Sí | @IsMongoId | Usuario con quien iniciar la conversación |
CreateGroupConversationDto (body de POST /group)
| Campo | Tipo | Obligatorio | Validaciones | Descripción |
|---|---|---|---|---|
name | string | Sí | @IsString, @MinLength(1), @MaxLength(100) | Nombre del grupo |
description | string | No | @IsString, @MaxLength(500) | Descripción |
photoFileId | string | No | @IsMongoId | Archivo usado como foto |
memberIds | string[] | Sí | @IsArray, @ArrayMinSize(1), @IsMongoId({ each: true }) | Miembros iniciales |
UpdateGroupConversationDto (body de PATCH /:id)
| Campo | Tipo | Obligatorio | Validaciones | Descripción |
|---|---|---|---|---|
name | string | No | @IsString, @MinLength(1), @MaxLength(100) | Nombre |
description | string | No | @IsString, @MaxLength(500) | Descripción |
photoFileId | string | No | @IsMongoId | Foto |
AddMembersDto (body de POST /:id/members)
| Campo | Tipo | Obligatorio | Validaciones | Descripción |
|---|---|---|---|---|
userIds | string[] | Sí | @IsArray, @ArrayMinSize(1), @IsMongoId({ each: true }) | Usuarios a agregar |
SendInternalMessageDto (body de POST /:id/messages)
| Campo | Tipo | Obligatorio | Validaciones | Descripción |
|---|---|---|---|---|
content | string | No | @IsString, @MaxLength(5000) | Texto |
contentType | string | No | @IsEnum: text, file, system | Si se omite: file cuando hay fileId, text en otro caso |
fileId | string | No | @IsMongoId | Archivo adjunto (debe existir en el tenant, si no 404) |
mentions | string[] | No | @IsArray, @IsMongoId({ each: true }) | Usuarios mencionados |
El servicio exige al menos content o fileId (400 en caso contrario).
QueryConversationDto (query de GET /internal-conversations)
Combina paginación y FilterConversationDto.
| Campo | Tipo | Obligatorio | Validaciones | Descripción |
|---|---|---|---|---|
page | number | No | @IsInt, @Min(1), @Type(() => Number) | Página |
limit | number | No | @IsInt, @Min(1), @Type(() => Number) | Tamaño |
type | string | No | @IsEnum: direct, group | Filtra por tipo |
search | string | No | @IsString | Búsqueda parcial por nombre de grupo |
QueryInternalMessagesDto (query de GET /:id/messages)
| Campo | Tipo | Obligatorio | Validaciones | Descripción |
|---|---|---|---|---|
page | number | No | @Type(() => Number), @IsInt, @Min(1) | Página (por defecto 1) |
limit | number | No | @Type(() => Number), @IsInt, @Min(1) | Tamaño (por defecto 20) |
QueryAgentsDto (query de GET /agents)
| Campo | Tipo | Obligatorio | Validaciones | Descripción |
|---|---|---|---|---|
departmentId | string | No | @IsMongoId | Filtra por departamento |
search | string | No | @IsString | Búsqueda parcial por nombre, apellido o email |
onlineOnly | boolean | No | @Transform(toBool), @IsBoolean | Solo agentes conectados en este momento |
WindowQueryDto (query de GET /:id/messages/:messageId/window)
Se comparte con el módulo de mensajes.
| Campo | Tipo | Obligatorio | Validaciones | Descripción |
|---|---|---|---|---|
radius | number | No | @Transform(toInt), @IsInt, @Min(1), @Max(50) | Mensajes antes y después del ancla (por defecto 15) |
Sockets
Los eventos los atiende ChatGateway (modules/chat/gateways/chat.gateway.ts) y los emite InternalChatBroadcastService. Cada usuario se une a la sala conversation:{id} y a las del tenant y del usuario.
Eventos que recibe el servidor
Todos exigen usuario autenticado en el socket y verifican la membresía (si no, emiten el evento de error del chat con internalChat.notFound).
| Evento | Payload (DTO) | Efecto |
|---|---|---|
im:join-conversation | ImConversationIdDto | Une el socket a conversation:{id} y responde im:conversation-joined |
im:leave-conversation | ImConversationIdDto | Saca el socket de la sala |
im:send-message | ImSendMessageDto | Guarda y difunde el mensaje; si trae tempId, responde im:new-message al remitente |
im:mark-read | ImConversationIdDto | Actualiza lastReadAt y difunde im:messages-read |
im:typing / im:stop-typing | ImConversationIdDto | Reenvía im:user-typing / im:user-stop-typing al resto de la sala |
ImConversationIdDto: conversationId (string, obligatorio, @IsMongoId, @IsNotEmpty).
ImSendMessageDto:
| Campo | Tipo | Obligatorio | Validaciones |
|---|---|---|---|
conversationId | string | Sí | @IsMongoId, @IsNotEmpty |
content | string | No | @IsString, @MaxLength(5000) |
contentType | string | No | @IsEnum: text, file, system |
fileId | string | No | @IsMongoId |
tempId | string | No | @IsString; id temporal del cliente para conciliar el mensaje |
mentions | string[] | No | @IsArray, @IsMongoId({ each: true }) |
Eventos que emite el servidor
im:conversation-created, im:conversation-updated, im:group-updated, im:member-added, im:member-removed, im:new-message, im:messages-read, im:presence-changed (a la sala del tenant, con { userId, online }), im:user-typing y im:user-stop-typing.
Envío de un mensaje
Servicios
ConversationService: creación de directos (condirectKey) y grupos, listado enriquecido (último mensaje, no leídos, presencia), membresía y lectura; genera mensajes de sistema al agregar, quitar o abandonar.InternalMessageService: validaciones y persistencia de mensajes, ventana alrededor de un ancla, y mensajes de sistema (member_addedagrupa todos los usuarios agregados en una sola llamada).InternalChatBroadcastService: emisión de eventos y cálculo de agentes conectados (getOnlineAgentIds, soloadminyagent).
Gotchas
POST /directyGET /agentsno exigen permiso; en cambioPOST /groupyPATCH /:idsí, aunque la creación de grupo además se limita a administradores en el servicio.- Los mensajes de sistema se crean en segundo plano: un fallo no revierte el cambio de membresía.
- Los mensajes destacados se gestionan en Starred Messages.
- Los ids de la ruta no se validan en el controlador; un id mal formado provoca un error de conversión.