Saltar al contenido
Chatbots de WhatsApp desde cero
Temario

Ejercicio: Webhooks y JSON de WhatsApp

  • ⏱ 50 minutos
  • 📶 Nivel: intermedio

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.

▶ Prueba tu código aquí (opcional)
Consola

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.

    funciones.js
    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;
    }