Ejercicio: Webhooks y JSON de WhatsApp
Objetivo
Construir con tus propias manos las cuatro piezas de la puerta de entrada de un bot: leer un payload sin romperse, clasificar eventos, responder la verificación del webhook y validar el formato y la igualdad de una firma.
Cómo se trabaja
La parte A se hace en tu computadora con el código de referencia del curso: ejecutas el parser y la firma sobre los ejemplos reales. La parte B son cinco funciones de JavaScript puro (sin import, sin Node) que se comprueban en esta página. No calculas el HMAC aquí (necesita crypto); lo que sí practicas es el formato de la cabecera y la comparación en tiempo constante, que son el resto de la lógica de la firma.
Los payloads de prueba tienen la forma payload.entry[].changes[].value.messages[] (mensajes) o .statuses[] (estados), como en la lección. Los teléfonos son falsos (51999000111).
💡 Reto extra
Escribe resumenDeEvento(payload) que devuelva un texto como "mensaje de 51999000111: Quiero 2 leches" o "estado delivered de wamid.S". Después, pásale test/payloads/estado-fallido.json y haz que incluya también el código de error. Es justo lo que querrás ver en los registros del servidor cuando algo falle.
Pasos
Marca cada paso a medida que lo completes. Tu avance se guarda en este navegador.
💡 Pista 1: Recorrer sin romperse
Usa payload?.entry ?? [], luego entrada?.changes ?? [] y cambio?.value?.messages ?? []. Comprueba además Array.isArray antes de iterar si quieres blindarlo contra valores raros como entry: "raro".
💡 Pista 2: Detectar «mensaje» o «estado»
Recorre todos los cambios y levanta dos banderas: hayMensajes si messages tiene longitud mayor a 0, hayEstados si statuses la tiene. Si hay mensajes, devuelve "mensaje"; si no y hay estados, "estado".
💡 Pista 3: La comparación en tiempo constante
Primero compara las longitudes. Después usa una variable diferencia = 0 y, por cada posición, diferencia |= a.charCodeAt(i) ^ b.charCodeAt(i). Al final, diferencia === 0. Nunca hagas return dentro del bucle.
💡 Pista 4: La expresión regular de la firma
/^sha256=[0-9a-f]{64}$/i.test(cabecera). Antes comprueba que cabecera sea de tipo string, porque test convertiría otros valores a texto.
🔎 Comprueba tu trabajo
Pega tu código y pulsa Comprobar. La revisión ocurre en tu navegador; nada se envía a ningún sitio. Cada punto en rojo incluye una pista. Pega solo el JavaScript, sin las etiquetas <script>.
✅ Ver una solución posible
Intenta resolverlo antes de mirar. Tu versión puede ser distinta y también correcta.
function cambiosDe(payload) {
var cambios = [];
var entradas = payload && Array.isArray(payload.entry) ? payload.entry : [];
for (var i = 0; i < entradas.length; i++) {
var lista = entradas[i] && Array.isArray(entradas[i].changes) ? entradas[i].changes : [];
for (var j = 0; j < lista.length; j++) cambios.push(lista[j]);
}
return cambios;
}
function tipoDeEvento(payload) {
var hayMensajes = false, hayEstados = false;
cambiosDe(payload).forEach(function (c) {
var v = (c && c.value) || {};
if (Array.isArray(v.messages) && v.messages.length > 0) hayMensajes = true;
if (Array.isArray(v.statuses) && v.statuses.length > 0) hayEstados = true;
});
if (hayMensajes) return "mensaje";
if (hayEstados) return "estado";
return "ignorar";
}
function extraerTexto(payload) {
var cambios = cambiosDe(payload);
for (var i = 0; i < cambios.length; i++) {
var v = (cambios[i] && cambios[i].value) || {};
var mensajes = Array.isArray(v.messages) ? v.messages : [];
for (var j = 0; j < mensajes.length; j++) {
var m = mensajes[j];
if (m && m.type === "text" && m.text && typeof m.text.body === "string") return m.text.body;
}
}
return null;
}
function responderVerificacion(params, tokenEsperado) {
var p = params || {};
var reto = p["hub.challenge"];
if (tokenEsperado && p["hub.mode"] === "subscribe" && p["hub.verify_token"] === tokenEsperado && reto) {
return { estado: 200, cuerpo: reto };
}
return { estado: 403, cuerpo: "Prohibido" };
}
function formatoFirmaValido(cabecera) {
return typeof cabecera === "string" && /^sha256=[0-9a-f]{64}$/i.test(cabecera);
}
function iguales(a, b) {
if (typeof a !== "string" || typeof b !== "string") return false;
if (a.length !== b.length) return false;
var diferencia = 0;
for (var i = 0; i < a.length; i++) diferencia |= a.charCodeAt(i) ^ b.charCodeAt(i);
return diferencia === 0;
}