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 autor el motivo y publica los retenidos cuando una persona los aprueba.
El formulario de comentarios es la primera puerta por la que entran el spam y el abuso en una web, y lo último que alguien quiere leerse entrada a entrada. En este artículo montamos un pequeño muro de comentarios en PHP puro, sin framework, y lo moderamos con ToxicFilter: cada comentario se comprueba antes de mostrarse, y pasa una de estas tres cosas.
- Allow: se publica en el acto. Es lo que ocurre 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/php-example.
Este artículo forma parte de una serie de cuatro que monta la misma aplicación, cada vez con un cliente distinto: Flask, Express 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
El cliente es un único paquete de Composer sin más dependencias que ext-curl:
composer require edulazaro/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.
TOXICFILTER_KEY=tf_test_... TOXICFILTER_WEBHOOK_SECRET=whsec_... php -S localhost:8000 -t public
Los comentarios se guardan en un fichero JSON (src/Comments.php), así que no hay base de datos que montar. Cámbialo por tu propio almacenamiento: nada de la moderación depende de él.
Moderar un comentario
Cuando se envía el formulario, el comentario se comprueba antes de guardarlo:
$id = $comments->nextId();
$tf = new Client(getenv('TOXICFILTER_KEY'));
try {
$verdict = $tf->text($body, [
'surface' => 'comment',
'reference' => "comment_{$id}", // how the webhook finds this comment later
]);
} catch (ApiError $e) {
// Nobody could judge it: hold it rather than publish it unread.
$comments->add($id, $name, $body, 'held', null);
header('Location: /?held=1', true, 303);
exit;
}
if ($verdict->blocked()) {
$error = $verdict->reason() ?? 'This comment cannot be published.';
} else {
$comments->add($id, $name, $body, $verdict->needsReview() ? 'held' : 'published', $verdict->id());
header('Location: /' . ($verdict->needsReview() ? '?held=1' : ''), true, 303);
exit;
}
}
}
En esas líneas hay cuatro cosas en las que merece la pena fijarse.
surfacedice dónde aparece el texto. Las políticas pueden tratar de forma distinta un comentario, un anuncio y un perfil, y así es como ToxicFilter sabe cuál es.referencees el id propio de la aplicación para el comentario. Vuelve con el veredicto y en cada webhook que se refiera a él, así que la aplicación nunca tiene que guardar los ids de ToxicFilter para encontrar sus propios comentarios.- No hay un booleano de "es tóxico".
blocked(),needsReview()yallowed()son las tres respuestas, y el veredicto trae además las categorías que superaron su umbral, todas las puntuaciones y la evidencia, por si quieres algo más que la decisión. - Si la API no responde, el comentario se retiene, no se publica. Qué hacer cuando falla es una decisión de cada sitio; en un formulario de comentarios, lo prudente es retener.
Explicarle al autor el motivo
Un comentario rechazado sin motivo es lo que hace que la moderación parezca arbitraria. Cada veredicto trae sus motivos, redactados para mostrárselos a quien escribió el comentario: reason() es el primero, reasons() todos. El formulario lo muestra bajo el comentario, escapado como todo lo demás que imprime la página:
<?php if ($error): ?><p class="error"><?= $e($error) ?></p><?php endif ?>
Cuando decide una persona: el webhook
Un comentario retenido espera en tu cola de revisión. Cuando alguien lo aprueba o lo rechaza ahí, ToxicFilter envía moderation.resolved a tu endpoint, firmado. El handler comprueba la firma, encuentra el comentario por su referencia y lo publica o lo descarta:
if ($method === 'POST' && $path === '/webhooks/toxicfilter') {
$event = Webhooks::event(
file_get_contents('php://input'), // the RAW body, before any parsing
$_SERVER['HTTP_X_TOXICFILTER_SIGNATURE'] ?? '',
getenv('TOXICFILTER_WEBHOOK_SECRET') ?: '',
);
if ($event === null) {
http_response_code(400);
exit;
}
if ($event['event'] === 'moderation.resolved' && preg_match('/^comment_(\d+)$/', $event['data']['reference'] ?? '', $m)) {
$event['data']['action'] === 'approved'
? $comments->publish((int) $m[1])
: $comments->remove((int) $m[1]);
}
http_response_code(204);
exit;
}
Webhooks::event() devuelve null para todo lo que no supera la verificación, incluida una cabecera ausente, así que una petición sin firmar es un 400 y nunca llega a los comentarios.
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 trae el veredicto y nunca el comentario: ToxicFilter no guarda lo que modera.
Lo que el cliente te da hecho
El cliente reintenta un límite de peticiones (429) y un error del servidor (5xx) con una espera creciente, y nunca reintenta un QuotaExhausted (402): uno significa "vuelve a intentarlo en un momento" y el otro "esta cuenta se ha quedado sin créditos", y cada caso pide justo lo contrario. Cada llamada lleva una clave de idempotencia, así que un reintento tras un timeout se juzga y se cobra una sola vez. Y nunca interpreta como allow nada que no sea una respuesta real: la página de error de un proxy o un cuerpo vacío son un error, no un veredicto. Los fallos son clases que puedes capturar una a una; todas extienden ApiError, que es lo que captura el muro.
El cliente completo está descrito en la documentación del SDK de PHP.
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, cola de revisión y 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 Express con el SDK de JavaScript
Construye en Express un muro de comentarios que publica, retiene o rechaza cada comentario con su motivo, mant...
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...