Moderar comentarios en Express con el SDK de JavaScript
Construye en Express un muro de comentarios que publica, retiene o rechaza cada comentario con su motivo, mantiene la clave en el servidor y verifica el webhook de revisión.
El formulario de comentarios es la primera puerta por la que entran el spam y el abuso, y lo último que alguien quiere leerse entrada a entrada. En este artículo montamos un pequeño muro de comentarios en Express y lo moderamos con ToxicFilter: cada comentario se comprueba antes de mostrarse, y pasa una de estas tres cosas.
- Allow: se publica al momento. Es lo que pasa con casi todos.
- Review: se retiene, y una persona decide en tu panel de ToxicFilter. Un webhook firmado avisa a la aplicación, que lo publica o lo descarta.
- Block: se rechaza, y al autor se le explica el motivo con palabras.
Tres resultados y no dos, a propósito. Si te obligan a elegir entre publicar y borrar, un umbral estricto borra comentarios legítimos y uno permisivo publica el abuso; en el resultado intermedio, los casos dudosos esperan a que los vea una persona. La aplicación completa está en toxicfilter/express-example.
Este artículo forma parte de una serie de cuatro que monta la misma aplicación, cada vez con un cliente distinto: PHP sin framework, Flask y Laravel con Laratox. Cada una tiene su propio repositorio, listo para clonar o para usar como plantilla: php-example, flask-example, express-example y laravel-example.
Instalación y configuración
npm install express toxicfilter-sdk
Crea una clave en tu panel; el plan gratuito basta. Una clave tf_test_ no cuesta nada y ejecuta todas las comprobaciones gratuitas, que son las que resuelven la mayoría de los comentarios, pero nunca consulta al modelo: usa una clave live para ver lo que aporta el modelo. La clave da acceso a toda la cuenta, así que va en una variable de entorno del servidor y nunca en una página.
Esto último importa más en JavaScript que en ningún otro sitio. El SDK también funciona en el navegador, y una clave en un bundle del navegador es pública: cualquiera que abra las herramientas de desarrollo puede gastarse los créditos de tu plan. Modera en el servidor, como hace esta aplicación, y envía el resultado a la página.
TOXICFILTER_KEY=tf_test_... TOXICFILTER_WEBHOOK_SECRET=whsec_... PORT=8000 npm start
Los comentarios se guardan en un fichero JSON (store.js), así que no hay base de datos que montar. Cámbialo por tu propio almacenamiento.
Moderar un comentario
const id = comments.nextId()
let verdict
try {
verdict = await tf.text(body, {
surface: 'comment',
reference: `comment_${id}`, // how the webhook finds this comment later
})
} catch (error) {
if (!(error instanceof ToxicFilterError)) throw error
// Nobody could judge it: hold it rather than publish it unread.
comments.add(id, name, body, 'held', null)
return res.redirect(303, '/?held=1')
}
if (verdict.blocked) {
return res.status(422).send(page({ name, body, error: verdict.reason ?? 'This comment cannot be published.' }))
}
comments.add(id, name, body, verdict.needsReview ? 'held' : 'published', verdict.id)
res.redirect(303, verdict.needsReview ? '/?held=1' : '/')
surfacedice dónde aparece el texto, para que una política pueda tratar un comentario de forma distinta que un perfil.referencees el id propio de la aplicación para el comentario, y vuelve en cada webhook que se refiera a él.- El veredicto responde con
blocked,needsReviewyallowed, nunca con un único booleano de "tóxico". - Si la API no puede responder, el comentario se retiene. Solo se captura
ToxicFilterError: un bug de la propia aplicación tiene que seguir fallando a la vista, no quedar archivado como "retenido".
Explicarle al autor el motivo
verdict.reason es el primer motivo, redactado para el autor; verdict.reasons los tiene todos. La página se genera con un template string, así que todo lo que viene del autor o de la API pasa antes por un escape(): un comentario es la vía más antigua para colarse en una página.
Cuando decide una persona: el webhook
app.post('/webhooks/toxicfilter', express.raw({ type: '*/*' }), async (req, res) => {
const event = await webhookEvent(
req.body,
req.get('X-ToxicFilter-Signature') ?? '',
process.env.TOXICFILTER_WEBHOOK_SECRET ?? '',
)
if (!event) return res.sendStatus(400)
const match = /^comment_(\d+)$/.exec(event.data?.reference ?? '')
if (event.event === 'moderation.resolved' && match) {
event.data.action === 'approved' ? comments.publish(Number(match[1])) : comments.remove(Number(match[1]))
}
res.sendStatus(204)
})
express.raw() en esa ruta conserva el cuerpo tal cual, con los bytes exactos que se firmaron; con un parser de JSON delante recibirías un objeto, y un objeto vuelto a codificar ya es otra cadena. webhookEvent() es asíncrona porque usa WebCrypto, el único HMAC que existe en todos los runtimes en los que funciona el SDK, y devuelve null para todo lo que no supera la verificación.
Para probarlo en tu máquina, expón la aplicación con un túnel (cloudflared tunnel --url http://localhost:8000, por ejemplo), añade la dirección del túnel seguida de /webhooks/toxicfilter como endpoint en Webhooks, y pon el secreto de firma que te muestra en TOXICFILTER_WEBHOOK_SECRET. Después publica un comentario que acabe en revisión, apruébalo desde la cola de revisión y verás cómo aparece.
Hay dos detalles que hacen que el handler no solo funcione, sino que sea correcto. La firma se comprueba sobre el cuerpo en bruto, porque un cuerpo parseado y vuelto a codificar ya es otra cadena y la firma nunca cuadraría. Y el comentario se encuentra por su reference, el id que la aplicación envió con la comprobación, porque el webhook lleva el veredicto y nunca el comentario: ToxicFilter no guarda lo que modera.
Lo que el cliente te da hecho
Reintenta los 429 y 5xx con una espera creciente y nunca reintenta QuotaExhausted; cada llamada lleva una clave de idempotencia; el timeout cubre el cuerpo además de las cabeceras, así que una respuesta atascada no deja colgada la petición; y solo un 2xx con una decisión dentro es un veredicto. No tiene dependencias (fetch se encarga del HTTP) y funciona en Node 20+, Deno, Bun y Cloudflare Workers. Todo está en la documentación del SDK de JavaScript.
Por dónde seguir
- Tus propias reglas: una política mueve los umbrales por categoría, añade tus propias palabras prohibidas o mide temas como el juego o las criptomonedas, que no son dañinos pero puede que no encajen en tu sitio. Indica cuál en la llamada.
- Varios sitios en una cuenta: dale a cada uno un proyecto, con su propia actividad, su cola de revisión y sus webhooks.
- Más allá del texto: el mismo cliente comprueba imágenes, nombres de usuario, registros completos y conversaciones, donde salen a la luz un acoso en grupo o un acercamiento que ningún mensaje deja ver por separado.
Sigue leyendo
Moderar comentarios en Flask con el SDK de Python
Monta un muro de comentarios en Flask que publica, retiene o rechaza cada comentario con su motivo, verifica e...
Moderar comentarios en Laravel con Laratox
Una regla de validación, una facade y un fake: construye en Laravel un muro de comentarios que publica, retien...
Moderar comentarios en PHP sin framework con el SDK de ToxicFilter
Sin framework: un muro de comentarios en PHP puro que publica, retiene o rechaza cada comentario, explica al a...