Skip to main content

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)​

CampoTipoDescripción
tenantIdObjectId → TenantTenant
typeenumdirect o group
name / descriptionstringSolo grupos
photoFileIdObjectId → FileFoto del grupo
createdByObjectId → UserCreador
directKeystringPara direct: los dos ids ordenados y unidos con :
lastMessageAt, lastMessagePreview, lastMessageSenderIdDate, string, ObjectIdResumen 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)​

CampoTipoDescripción
tenantId, conversationId, userIdObjectIdRelación
roleenumadmin o member (por defecto member)
lastReadAtDateÚltima lectura del usuario
joinedAtDateFecha de ingreso

Índice único (conversationId, userId).

InternalMessage (colección internal_messages)​

CampoTipoDescripción
tenantId, conversationIdObjectIdRelación
senderIdObjectId → UserAusente en mensajes de sistema
contentstringTexto
contentTypeenumtext (por defecto), file o system
fileIdObjectId → FileAdjunto
mentionsObjectId[] → UserUsuarios mencionados
systemEventsubdocumentotype (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étodoRutaPermisoDescripción
POST/internal-conversations/directSolo sesiónCrea (o recupera) la conversación 1:1 con otro usuario
POST/internal-conversations/groupINTERNAL_CHAT_CREATECrea un grupo (el servicio exige userType = admin)
GET/internal-conversationsSolo sesiónMis conversaciones, paginadas
GET/internal-conversations/agentsSolo sesiónAgentes y administradores del tenant, con presencia
GET/internal-conversations/:idSolo sesiónDetalle de una conversación (data.results con un elemento)
PATCH/internal-conversations/:idINTERNAL_CHAT_UPDATEEdita nombre, descripción o foto de un grupo
GET/internal-conversations/:id/messagesSolo sesiónMensajes, paginados
POST/internal-conversations/:id/messagesSolo sesiónEnvía un mensaje
GET/internal-conversations/:id/messages/:messageId/windowSolo sesiónVentana de mensajes alrededor de uno (para saltar a un destacado)
GET/internal-conversations/:id/membersSolo sesiónMiembros con datos de usuario y presencia
POST/internal-conversations/:id/membersSolo sesión (el servicio exige administrador)Agrega miembros a un grupo
DELETE/internal-conversations/:id/members/:userIdSolo sesión (el servicio exige administrador)Quita un miembro
POST/internal-conversations/:id/leaveSolo sesiónAbandona un grupo
POST/internal-conversations/:id/readSolo sesiónMarca 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 como admin del grupo.
  • Agregar o quitar miembros y editar grupos: un administrador del tenant (userType = admin) o un miembro con rol admin del 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 direct no se puede abandonar (400).
  • Los usuarios agregados deben pertenecer al tenant.

DTOs​

Validaciones de class-validator. "Opcional" indica @IsOptional().

Parámetros de ruta​

ParámetroTipoObligatorioValidaciónUsado en
idstringSíNinguna en el controladorTodas las rutas con :id
messageIdstringSíNinguna:id/messages/:messageId/window
userIdstringSíNingunaDELETE :id/members/:userId

CreateDirectConversationDto (body de POST /direct)​

CampoTipoObligatorioValidacionesDescripción
targetUserIdstringSí@IsMongoIdUsuario con quien iniciar la conversación

CreateGroupConversationDto (body de POST /group)​

CampoTipoObligatorioValidacionesDescripción
namestringSí@IsString, @MinLength(1), @MaxLength(100)Nombre del grupo
descriptionstringNo@IsString, @MaxLength(500)Descripción
photoFileIdstringNo@IsMongoIdArchivo usado como foto
memberIdsstring[]Sí@IsArray, @ArrayMinSize(1), @IsMongoId({ each: true })Miembros iniciales

UpdateGroupConversationDto (body de PATCH /:id)​

CampoTipoObligatorioValidacionesDescripción
namestringNo@IsString, @MinLength(1), @MaxLength(100)Nombre
descriptionstringNo@IsString, @MaxLength(500)Descripción
photoFileIdstringNo@IsMongoIdFoto

AddMembersDto (body de POST /:id/members)​

CampoTipoObligatorioValidacionesDescripción
userIdsstring[]Sí@IsArray, @ArrayMinSize(1), @IsMongoId({ each: true })Usuarios a agregar

SendInternalMessageDto (body de POST /:id/messages)​

CampoTipoObligatorioValidacionesDescripción
contentstringNo@IsString, @MaxLength(5000)Texto
contentTypestringNo@IsEnum: text, file, systemSi se omite: file cuando hay fileId, text en otro caso
fileIdstringNo@IsMongoIdArchivo adjunto (debe existir en el tenant, si no 404)
mentionsstring[]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.

CampoTipoObligatorioValidacionesDescripción
pagenumberNo@IsInt, @Min(1), @Type(() => Number)Página
limitnumberNo@IsInt, @Min(1), @Type(() => Number)Tamaño
typestringNo@IsEnum: direct, groupFiltra por tipo
searchstringNo@IsStringBúsqueda parcial por nombre de grupo

QueryInternalMessagesDto (query de GET /:id/messages)​

CampoTipoObligatorioValidacionesDescripción
pagenumberNo@Type(() => Number), @IsInt, @Min(1)Página (por defecto 1)
limitnumberNo@Type(() => Number), @IsInt, @Min(1)Tamaño (por defecto 20)

QueryAgentsDto (query de GET /agents)​

CampoTipoObligatorioValidacionesDescripción
departmentIdstringNo@IsMongoIdFiltra por departamento
searchstringNo@IsStringBúsqueda parcial por nombre, apellido o email
onlineOnlybooleanNo@Transform(toBool), @IsBooleanSolo agentes conectados en este momento

WindowQueryDto (query de GET /:id/messages/:messageId/window)​

Se comparte con el módulo de mensajes.

CampoTipoObligatorioValidacionesDescripción
radiusnumberNo@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).

EventoPayload (DTO)Efecto
im:join-conversationImConversationIdDtoUne el socket a conversation:{id} y responde im:conversation-joined
im:leave-conversationImConversationIdDtoSaca el socket de la sala
im:send-messageImSendMessageDtoGuarda y difunde el mensaje; si trae tempId, responde im:new-message al remitente
im:mark-readImConversationIdDtoActualiza lastReadAt y difunde im:messages-read
im:typing / im:stop-typingImConversationIdDtoReenvía im:user-typing / im:user-stop-typing al resto de la sala

ImConversationIdDto: conversationId (string, obligatorio, @IsMongoId, @IsNotEmpty).

ImSendMessageDto:

CampoTipoObligatorioValidaciones
conversationIdstringSí@IsMongoId, @IsNotEmpty
contentstringNo@IsString, @MaxLength(5000)
contentTypestringNo@IsEnum: text, file, system
fileIdstringNo@IsMongoId
tempIdstringNo@IsString; id temporal del cliente para conciliar el mensaje
mentionsstring[]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 (con directKey) 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_added agrupa todos los usuarios agregados en una sola llamada).
  • InternalChatBroadcastService: emisión de eventos y cálculo de agentes conectados (getOnlineAgentIds, solo admin y agent).

Gotchas​

  • POST /direct y GET /agents no exigen permiso; en cambio POST /group y PATCH /:id sí, 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.