Saltar al contenido principal

Moderación de contenido

Los administradores pueden revisar la cola de contenido marcado, actuar sobre contenido reportado y gestionar la cola de revisión de reportes orientada al usuario, además de un motor de reglas de auto-moderación real (este documento anteriormente decía que la auto-moderación estaba "declarada en el esquema pero no implementada" — corregido, ver más abajo). Conectado a las páginas dedicadas /moderation/flagged y /moderation/rules.

Checklist de implementación

Contenido marcado y acciones

  • Listar/paginar la cola de contenido marcado (adminGetFlaggedContent - corregido en esta sesión: requería un archivo data-access-services/content-flags.access-service.js que no existe en ningún lugar, y su fallback llamaba a contentReportManager.getContentReports() con la firma de argumentos incorrecta - ambas rutas siempre lanzaban error. No existe un concepto separado de "content flag" implementado en ningún lugar del backend; ahora esto está construido directamente sobre el mismo access service paginado de ContentReport que usa adminGetReports. Conectado a la página /moderation/flagged.)
  • Obtener el contexto de moderación de un elemento marcado (adminGetContentDetails - conectado al panel de detalle "Ver" de la página /moderation/flagged. Corregido en esta sesión, varios bugs introducidos por el cambio de forma de adminGetFlaggedContent mencionado arriba: llamaba a this.getModerationHistory(contentId, {}, context) contra un método cuya firma real es (contentType, contentId, context), así que pasaba el id de contenido y un objeto vacío en las posiciones equivocadas; llamaba a contentReportManager.getContentReports() con un objeto de filtros y context como sus dos primeros argumentos en lugar de (contentType, contentId, options, context); y llamaba a .some() directamente sobre el nuevo valor de retorno {content, total, limit, offset} de getFlaggedContent() en lugar de sobre su arreglo .content - los tres casos fallaban silenciosamente sin lanzar error, gracias a los bloques try/catch que los rodeaban. También se agregaron los campos requeridos no-nulos id/status del esquema, que faltaban por completo. Por separado, ContentModerationDetails.nsfwScore - antes siempre null, ya que nada lo asignaba - ahora tiene un field resolver real en content-moderation.resolver.js que consulta data-access-services/admin/post-nsfw-score.access-service.js para el contenido y devuelve el peor puntaje de confianza; solo está poblado para publicaciones, obtenido del escaneo de nsfw-detection.manager.js en la creación de la publicación. El panel de detalle de la página /moderation/flagged ya tenía un badge para este valor, simplemente nunca había recibido datos antes. ContentModerationDetails.toxicityScore - también antes siempre null - ahora también tiene un field resolver real: devuelve NsfwCommentScore.toxicityScore para contentType: COMMENT (el único tipo de contenido donde realmente se persiste una señal de toxicidad, vía database/models/NsfwCommentScore.js) y se mantiene en null para publicaciones y todo lo demás, ya que no existe un modelo de toxicidad equivalente para publicaciones - eso es intencional, no una carencia pendiente.)
  • Obtener historial de moderación de un elemento de contenido (adminGetContentModerationHistory - conectado al panel de detalle de la página /moderation/flagged, y ahora sí devuelve datos: data-access-services/content-moderation-log.access-service.js implementa findByContent(contentType, contentId, options) contra la tabla real content_moderation_logs (migración 20260717060000-create-content-moderation-logs.js), de la cual lee getModerationHistory(). Este documento decía antes que findByContent no existía y que el endpoint siempre devolvía [] - eso ya no es así, y está cubierto por la suite getModerationHistory de tests/unit-test/admin-content-moderation.unit.test.js. Corregido en esta sesión: además de eso, las filas del registro se mapeaban a {id, action, moderatorId, moderatorType, previousStatus, newStatus, metadata, createdAt}, una forma que no existe en absoluto en el tipo ModerationAction del esquema (actionType/reason/performedBy/performedAt/notes) - así que cada llamada real lanzaba un error de serialización de GraphQL en los campos no-nulos actionType/performedBy/performedAt, incluso con findByContent devolviendo filas reales. Ahora se mapea a los nombres de campo correctos, y las entradas cuyo moderatorId no se puede resolver a un admin real (por ejemplo la auto-moderación con 'system') se descartan en lugar de violar la restricción no-nula de performedBy.)
  • Estadísticas de moderación de contenido por período (adminGetContentModerationStats - conectado a las tarjetas de estadísticas de la página /moderation/flagged. Corregido en esta sesión: getContentStats() trataba el nuevo valor de retorno {content, total, limit, offset} de getFlaggedContent() como un arreglo simple (.length, .forEach) - el mismo tipo de bug que adminGetContentDetails arriba.)
  • Contenido en tendencia/viral para revisión (adminGetTrendingContent - conectado a una sección colapsable "Contenido en tendencia" en la página /moderation/flagged, con una acción de "marcar para revisión" de un clic por fila que reutiliza el flujo de modal ya existente de adminFlagContent. Corregido en esta sesión: getTrendingContent() ahora también tiene una rama para contentType: COMMENT — no existe un equivalente de getTrendingPosts de postAccessService para PostComment, así que esta rama consulta PostComment directamente dentro de la misma ventana temporal, puntuando sobre likesCount/repliesCount con los mismos pesos relativos que usa la rama de posts para likes/comentarios. Antes esta rama no existía en absoluto, así que pedir contentType: "COMMENT" devolvía silenciosamente una lista vacía.)
  • Aprobar contenido marcado (adminApproveContent - resolver protegido por MODERATE_CONTENT → contentModerationManager.approveContent, que limpia la marca y registra la decisión)
  • Rechazar contenido marcado (adminRejectContent - resolver protegido por REMOVE_CONTENT → contentModerationManager.rejectContent, que elimina el contenido y registra la decisión)
  • Eliminar contenido que infringe las normas (adminRemoveContent - conectado a la acción "Eliminar" de la página /moderation/flagged; la página de moderación (cola de reportes) elimina contenido por separado de forma indirecta mediante el flag removeContent de adminReviewReport)
  • Restaurar contenido previamente eliminado (adminRestoreContent - conectado a la acción "Restaurar" de la página /moderation/flagged)
  • Eliminar contenido en lote (solo super_admin) (adminBulkRemoveContent - conectado a la acción de selección en lote de la página /moderation/flagged, solo super_admin)
  • Marcar contenido para revisión (adminFlagContent - conectado a la acción "Marcar" de la página /moderation/flagged)
  • Desmarcar contenido (adminUnflagContent - conectado a la acción "Desmarcar" de la página /moderation/flagged)

Reglas de auto-moderación

Corregido en esta revisión — esta sección anteriormente decía que las cuatro operaciones estaban "declaradas pero ningún resolver las implementa". Eso era incorrecto: existen resolvers reales en graphql/resolvers/admin/content-moderation.resolver.js, que delegan a un CRUD real en managers/admin-managers/content-moderation.manager.js contra una tabla real de base de datos auto_moderation_rule (modelo AutoModerationRule.js), y hay una interfaz CRUD real y funcional en /moderation/rules (tabla + modal de creación/edición) que ejercita las cuatro.

Las reglas además realmente se aplican, no solo se almacenan — services/auto-moderation.service.js es un motor de reglas que compara los tipos de regla keyword/regex/nsfw_score/report_threshold contra el contenido y aplica las acciones flag/remove/warn. Se llama (de forma best-effort, sin bloquear) desde post.manager.js en cada creación de publicación. No se volvió a verificar en esta revisión si los tipos de regla nsfw_score/report_threshold se evalúan en otras rutas (el escaneo NSFW, la creación de reportes).

Advertencias de contenido

  • Agregar una etiqueta de advertencia de contenido (adminAddContentWarning - corregido en esta sesión: el resolver desestructuraba warningType/message mientras que el esquema declara warning/severity - las llamadas reales pasaban undefined silenciosamente para ambos. warning se mapea al warningType del manager (debe ser uno de SENSITIVE/GRAPHIC/MISINFORMATION/VIOLENCE/ADULT), severity se mapea a su message de texto libre. Conectado a la acción "Advertencia" de la página /moderation/flagged. A diferencia de admin-user.resolver.js y content-report.resolver.js, content-moderation.resolver.js no tiene un duplicado real de nivel superior que mantener sincronizado - solo existe la copia de admin/ en el disco.)
  • Eliminar una advertencia de contenido (adminRemoveContentWarning - un elemento de contenido tiene como máximo una advertencia activa a la vez (columnas planas en la fila, no una tabla por advertencia - ver database/migrations/20260730180000-add-content-warning-to-post.js), así que el argumento warningId: ID! del esquema que este documento señalaba antes como ignorado no tenía nada que buscar en primer lugar; se eliminó del esquema, coincidiendo con lo que el resolver ya hacía. ContentModerationDetails ahora expone hasWarning/warningType/warningMessage, y el panel de detalle de la página /moderation/flagged muestra un botón "Quitar advertencia" siempre que hay una advertencia activa.)

Cola de revisión de reportes

  • Listar/filtrar reportes (adminGetReports - backend listo, conectado a la página de moderación; la misma lógica también está duplicada literalmente en el resolver orientado al cliente resolvers/content-report.resolver.js)
  • Listar reportes por usuario (adminGetReportsByUser - lista reportes presentados por un usuario, vía getByReporterPaginated; conectado a una tarjeta "Reportes presentados por este usuario" en la página /users/[id])
  • Estadísticas de reportes por tipo/motivo (adminGetReportStats - conectado a las tarjetas de estadísticas en la parte superior de la página de moderación)
  • Detalle completo de un reporte con historial del reportante (adminGetReportDetails - conectado a un modal "Ver detalles" por reporte en la página de moderación, que muestra los otros reportes del reportante y otros reportes sobre el mismo contenido - útil para detectar un reportante en serie o una acumulación de alta prioridad)
  • Aprobar/rechazar/escalar un reporte (adminReviewReport - backend listo, conectado a los botones Aprobar/Rechazar/Escalar de la página de moderación, todos enrutados a través de esta única mutation vía el parámetro action)
  • Revisar múltiples reportes en lote (adminBulkReviewReports - conectado a la barra de selección múltiple de la página de moderación, junto a los botones Aprobar/Rechazar/Escalar por fila; este documento decía antes que ninguna interfaz de frontend-admin lo usaba — corregido)
  • Escalar un reporte a otro administrador (ya no es una mutation de GraphQL distinta - adminEscalateReport fue eliminada tanto de graphql/types/admin/content-report.type.js como del archivo orientado al cliente graphql/types/content-report.type.js (y sus resolvers fueron eliminados), así que ya no existe en absoluto en el esquema. content-report.manager.js todavía tiene un método escalateReport(), pero ahora es código muerto inalcanzable - nada lo llama. La escalación en la práctica ocurre solo a través de adminReviewReport con action: ESCALATE (que establece el estado del reporte en reviewing), que es lo que el botón "Escalar" de la página de moderación ya usaba.)
  • Descartar un reporte (tampoco es ya una mutation de GraphQL distinta - adminDismissReport fue eliminada del esquema junto con adminEscalateReport, en el mismo commit. El método dismissReport() de content-report.manager.js todavía existe pero también es código muerto inalcanzable. El descarte en la práctica ocurre mediante adminReviewReport con action: REJECT, que establece el estado del reporte en dismissed.)

Controles de comentarios

  • Desactivar comentarios en un elemento de contenido (adminDisableComments - implementado dentro de content-moderation.resolver.js en lugar de un archivo de resolver dedicado; conectado a un botón de alternancia en el panel de detalle de la página /moderation/flagged, junto con isCommentsDisabled expuesto en ContentModerationDetails. Solo las filas de tipo POST tienen la columna subyacente, igual que las advertencias de contenido.)
  • Activar comentarios en un elemento de contenido (adminEnableComments - mismo botón de alternancia, cambia según el estado actual de isCommentsDisabled del contenido)

Referencia técnica

Ver Panel de administración → Moderación de contenido, Cola de revisión de reportes, y Controles de comentarios para la API completa de GraphQL. Para el lado orientado al usuario del reporte de contenido (enviar un reporte), ver Moderación de Contenido y Reportes.

Una nota sobre un bug relacionado del esquema de cliente encontrado en esta sesión: la mutation orientada al cliente updateReportStatus (graphql/types/content-report.type.js / graphql/resolvers/content-report.resolver.js, restringida a administradores pero no parte del esquema de administrador) comparte el método updateReportStatus() de content-report.manager.js, el cual tenía exactamente el mismo bug de reviewNotes vs adminNotes que adminEscalateReport/adminDismissReport arriba - escribía a un atributo reviewNotes inexistente en lugar de la columna real adminNotes del modelo, así que las notas de revisión se descartaban silenciosamente en cada llamada. Corregido junto con los bugs del esquema de administrador ya que es el mismo método del manager.