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 archivodata-access-services/content-flags.access-service.jsque no existe en ningún lugar, y su fallback llamaba acontentReportManager.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 deContentReportque usaadminGetReports. 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 deadminGetFlaggedContentmencionado arriba: llamaba athis.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 acontentReportManager.getContentReports()con un objeto de filtros ycontextcomo 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}degetFlaggedContent()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-nulosid/statusdel esquema, que faltaban por completo. Por separado,ContentModerationDetails.nsfwScore- antes siemprenull, ya que nada lo asignaba - ahora tiene un field resolver real encontent-moderation.resolver.jsque consultadata-access-services/admin/post-nsfw-score.access-service.jspara el contenido y devuelve el peor puntaje de confianza; solo está poblado para publicaciones, obtenido del escaneo densfw-detection.manager.jsen la creación de la publicación. El panel de detalle de la página/moderation/flaggedya tenía un badge para este valor, simplemente nunca había recibido datos antes.ContentModerationDetails.toxicityScore- también antes siemprenull- ahora también tiene un field resolver real: devuelveNsfwCommentScore.toxicityScoreparacontentType: COMMENT(el único tipo de contenido donde realmente se persiste una señal de toxicidad, víadatabase/models/NsfwCommentScore.js) y se mantiene ennullpara 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.jsimplementafindByContent(contentType, contentId, options)contra la tabla realcontent_moderation_logs(migración20260717060000-create-content-moderation-logs.js), de la cual leegetModerationHistory(). Este documento decía antes quefindByContentno existía y que el endpoint siempre devolvía[]- eso ya no es así, y está cubierto por la suitegetModerationHistorydetests/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 tipoModerationActiondel esquema (actionType/reason/performedBy/performedAt/notes) - así que cada llamada real lanzaba un error de serialización de GraphQL en los campos no-nulosactionType/performedBy/performedAt, incluso confindByContentdevolviendo filas reales. Ahora se mapea a los nombres de campo correctos, y las entradas cuyomoderatorIdno 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 deperformedBy.) - 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}degetFlaggedContent()como un arreglo simple (.length,.forEach) - el mismo tipo de bug queadminGetContentDetailsarriba.) - 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 deadminFlagContent. Corregido en esta sesión:getTrendingContent()ahora también tiene una rama paracontentType: COMMENT— no existe un equivalente degetTrendingPostsdepostAccessServiceparaPostComment, así que esta rama consultaPostCommentdirectamente dentro de la misma ventana temporal, puntuando sobrelikesCount/repliesCountcon los mismos pesos relativos que usa la rama de posts para likes/comentarios. Antes esta rama no existía en absoluto, así que pedircontentType: "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 flagremoveContentdeadminReviewReport) - 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, solosuper_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).
- Listar reglas de auto-moderación (
adminGetAutoModerationRules- resolver + manager + tabla de base de datos reales, conectado a la página/moderation/rules) - Crear una regla de auto-moderación (
adminCreateAutoModerationRule- conectado al modal de creación de la página/moderation/rules) - Actualizar una regla de auto-moderación (
adminUpdateAutoModerationRule- conectado al modal de edición de la página/moderation/rules) - Eliminar una regla de auto-moderación (
adminDeleteAutoModerationRule- conectado a la página/moderation/rules) - Estadísticas agregadas de moderación (variante sin argumentos) (
adminGetModerationStats- resolver protegido por VIEW_ANALYTICS →contentModerationManager.getContentStats({}), el mismo método funcional detrás deadminGetContentModerationStats)
Advertencias de contenido
- Agregar una etiqueta de advertencia de contenido (
adminAddContentWarning- corregido en esta sesión: el resolver desestructurabawarningType/messagemientras que el esquema declarawarning/severity- las llamadas reales pasabanundefinedsilenciosamente para ambos.warningse mapea alwarningTypedel manager (debe ser uno deSENSITIVE/GRAPHIC/MISINFORMATION/VIOLENCE/ADULT),severityse mapea a sumessagede texto libre. Conectado a la acción "Advertencia" de la página/moderation/flagged. A diferencia deadmin-user.resolver.jsycontent-report.resolver.js,content-moderation.resolver.jsno tiene un duplicado real de nivel superior que mantener sincronizado - solo existe la copia deadmin/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 - verdatabase/migrations/20260730180000-add-content-warning-to-post.js), así que el argumentowarningId: 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.ContentModerationDetailsahora exponehasWarning/warningType/warningMessage, y el panel de detalle de la página/moderation/flaggedmuestra 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 clienteresolvers/content-report.resolver.js) - Listar reportes por usuario (
adminGetReportsByUser- lista reportes presentados por un usuario, víagetByReporterPaginated; 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ámetroaction) - 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 -
adminEscalateReportfue eliminada tanto degraphql/types/admin/content-report.type.jscomo del archivo orientado al clientegraphql/types/content-report.type.js(y sus resolvers fueron eliminados), así que ya no existe en absoluto en el esquema.content-report.manager.jstodavía tiene un métodoescalateReport(), pero ahora es código muerto inalcanzable - nada lo llama. La escalación en la práctica ocurre solo a través deadminReviewReportconaction: ESCALATE(que establece el estado del reporte enreviewing), 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 -
adminDismissReportfue eliminada del esquema junto conadminEscalateReport, en el mismo commit. El métododismissReport()decontent-report.manager.jstodavía existe pero también es código muerto inalcanzable. El descarte en la práctica ocurre medianteadminReviewReportconaction: REJECT, que establece el estado del reporte endismissed.)
Controles de comentarios
- Desactivar comentarios en un elemento de contenido (
adminDisableComments- implementado dentro decontent-moderation.resolver.jsen 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 conisCommentsDisabledexpuesto enContentModerationDetails. 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 deisCommentsDisableddel 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.