Configuración de la integración de chat de HubSpot

¿Quieres integrar un CSM en Discourse? ¡Veamos cómo integrar el chat de HubSpot en Discourse!

  1. Crea una cuenta en HubSpot

  2. Selecciona Chat

  3. Personaliza la interfaz y la disponibilidad como desees


  4. Copia el código

  5. Crea un nuevo componente de tema y pégalo en la pestaña Common - </body>. Añade el nuevo componente a tus temas principales.

  6. Completa la Verificación en Hubspot para activar el widget y ve a tu sitio web

  7. ¡Hecho! :tada:

12 Me gusta

Hola Daniella, he probado esta integración con tres proveedores de chat diferentes y sigo obteniendo el mismo error de CSP después de agregar todos los enlaces que pude encontrar a script-src de la política de seguridad. Este en particular es para Tidio, pero ocurrió lo mismo con LiveChat y Pure Chat. ¿Tienes alguna idea de lo que podría estar sucediendo?

Content Security Policy: La configuración de la página bloqueó la carga de un recurso en eval ("script-src"). Origen: (function injected(eventName, injectedIntoContentWindow)
{
  let checkRequest;

  /*
   * Envoltorio del contexto del marco
   *
   * Para algunos casos extremos, Chrome no ejecutará scripts de contenido dentro de marcos.
   * Los sitios web han comenzado a abusar de este hecho para acceder a APIs sin envolver a través del
   * contentWindow de un marco (#4586, 5207). Por lo tanto, hasta que Chrome ejecute scripts de
   * contenido de manera consistente para todos los marcos, debemos asegurarnos de (re)inyectar nuestros
   * envoltorios cuando se acceda al contentWindow.
   */
  let injectedToString = Function.prototype.toString.bind(injected);
  let injectedFrames = new WeakSet();
  let injectedFramesAdd = WeakSet.prototype.add.bind(injectedFrames);
  let injectedFramesHas = WeakSet.prototype.has.bind(injectedFrames);

  function injectIntoContentWindow(contentWindow)
  {
    if (contentWindow && !injectedFramesHas(contentWindow))
    {
      injectedFramesAdd(contentWindow);
      try
      {
        contentWindow[eventName] = checkRequest;
        contentWindow.eval(
          "(" + injectedToString() + ")('" + eventName + "', true);"
        );
        delete contentWindow[eventName];
      }
      catch (e) {}
    }
  }

  for (let element of [HTMLFrameElement, HTMLIFrameElement, HTMLObjectElement])
  {
    let contentDocumentDesc = Object.getOwnPropertyDescriptor(
      element.prototype, "contentDocument"
    );
    let contentWindowDesc = Object.getOwnPropertyDescriptor(
      element.prototype, "contentWindow"
    );

    // Aparentemente, en HTMLObjectElement.prototype.contentWindow no existe
    // en versiones antiguas de Chrome como la 51.
    if (!contentWindowDesc)
      continue;

    let getContentDocument = Function.prototype.call.bind(
      contentDocumentDesc.get
    );
    let getContentWindow = Function.prototype.call.bind(
      contentWindowDesc.get
    );

    contentWindowDesc.get = function()
    {
      let contentWindow = getContentWindow(this);
      injectIntoContentWindow(contentWindow);
      return contentWindow;
    };
    contentDocumentDesc.get = function()
    {
      injectIntoContentWindow(getContentWindow(this));
      return getContentDocument(this);
    };
    Object.defineProperty(element.prototype, "contentWindow",
                          contentWindowDesc);
    Object.defineProperty(element.prototype, "contentDocument",
                          contentDocumentDesc);
  }

  /*
   * Envoltorio RTCPeerConnection
   *
   * La API webRequest en Chrome aún no permite bloquear
   * las conexiones WebRTC.
   * Ver https://bugs.chromium.org/p/chromium/issues/detail?id=707683
   */
  let RealCustomEvent = window.CustomEvent;

  // Si hemos sido inyectados en un marco a través de contentWindow, entonces podemos simplemente
  // tomar la copia de checkRequest dejada para nosotros por el documento padre. De lo contrario,
  // necesitamos configurarlo ahora, junto con las funciones de manejo de eventos.
  if (injectedIntoContentWindow)
    checkRequest = window[eventName];
  else
  {
    let addEventListener = document.addEventListener.bind(document);
    let dispatchEvent = document.dispatchEvent.bind(document);
    let removeEventListener = document.removeEventListener.bind(document);
    checkRequest = (url, callback) =>
    {
      let incomingEventName = eventName + "-" + url;

      function listener(event)
      {
        callback(event.detail);
        removeEventListener(incomingEventName, listener);
      }
      addEventListener(incomingEventName, listener);

      dispatchEvent(new RealCustomEvent(eventName, {detail: {url}}));
    };
  }

  // Solo debe llamarse antes del código de la página, no endurecido.
  function copyProperties(src, dest, properties)
  {
    for (let name of properties)
    {
      if (Object.prototype.hasOwnProperty.call(src, name))
      {
        Object.defineProperty(dest, name,
                              Object.getOwnPropertyDescriptor(src, name));
      }
    }
  }

  let RealRTCPeerConnection = window.RTCPeerConnection ||
                              window.webkitRTCPeerConnection;

  // Firefox tiene la opción (media.peerconnection.enabled) para deshabilitar WebRTC
  // en cuyo caso RealRTCPeerConnection es indefinido.
  if (typeof RealRTCPeerConnection != "undefined")
  {
    let closeRTCPeerConnection = Function.prototype.call.bind(
      RealRTCPeerConnection.prototype.close
    );
    let RealArray = Array;
    let RealString = String;
    let {create: createObject, defineProperty} = Object;

    let normalizeUrl = url =>
    {
      if (typeof url != "undefined")
        return RealString(url);
    };

    let safeCopyArray = (originalArray, transform) =>
    {
      if (originalArray == null || typeof originalArray != "object")
        return originalArray;

      let safeArray = RealArray(originalArray.length);
      for (let i = 0; i < safeArray.length; i++)
      {
        defineProperty(safeArray, i, {
          configurable: false, enumerable: false, writable: false,
          value: transform(originalArray[i])
        });
      }
      defineProperty(safeArray, "length", {
        configurable: false, enumerable: false, writable: false,
        value: safeArray.length
      });
      return safeArray;
    };

    // Sería mucho más fácil usar el método .getConfiguration para obtener
    // la configuración normalizada y segura desde la instancia de RTCPeerConnection.
    // Desafortunadamente, no está implementado en Chrome inestable 59.
    // Ver https://www.chromestatus.com/feature/5271355306016768
    let protectConfiguration = configuration =>
    {
      if (configuration == null || typeof configuration != "object")
        return configuration;

      let iceServers = safeCopyArray(
        configuration.iceServers,
        iceServer =>
        {
          let {url, urls} = iceServer;

          // RTCPeerConnection no itera a través de pseudo arrays de urls.
          if (typeof urls != "undefined" && !(urls instanceof RealArray))
            urls = [urls];

          return createObject(iceServer, {
            url: {
              configurable: false, enumerable: false, writable: false,
              value: normalizeUrl(url)
            },
            urls: {
              configurable: false, enumerable: false, writable: false,
              value: safeCopyArray(urls, normalizeUrl)
            }
          });
        }
      );

      return createObject(configuration, {
        iceServers: {
          configurable: false, enumerable: false, writable: false,
          value: iceServers
        }
      });
    };

    let checkUrl = (peerconnection, url) =>
    {
      checkRequest(url, blocked =>
      {
        if (blocked)
        {
          // Llamar a .close() lanza un error si ya está cerrado.
          try
          {
            closeRTCPeerConnection(peerconnection);
          }
          catch (e) {}
        }
      });
    };

    let checkConfiguration = (peerconnection, configuration) =>
    {
      if (configuration && configuration.iceServers)
      {
        for (let i = 0; i < configuration.iceServers.length; i++)
        {
          let iceServer = configuration.iceServers[i];
          if (iceServer)
          {
            if (iceServer.url)
              checkUrl(peerconnection, iceServer.url);

            if (iceServer.urls)
            {
              for (let j = 0; j < iceServer.urls.length; j++)
                checkUrl(peerconnection, iceServer.urls[j]);
            }
          }
        }
      }
    };

    // Chrome inestable (probado con 59) ya ha implementado
    // setConfiguration, por lo que necesitamos envolver eso si también existe.
    // https://www.chromestatus.com/feature/5596193748942848
    if (RealRTCPeerConnection.prototype.setConfiguration)
    {
      let realSetConfiguration = Function.prototype.call.bind(
        RealRTCPeerConnection.prototype.setConfiguration
      );

      RealRTCPeerConnection.prototype.setConfiguration = function(configuration)
      {
        configuration = protectConfiguration(configuration);

        // Llama al método real primero, para que valide la configuración por
        // nosotros. También podríamos hacerlo ya que checkRequest es asíncrono de todos modos.
        realSetConfiguration(this, configuration);
        checkConfiguration(this, configuration);
      };
    }

    let WrappedRTCPeerConnection = function(...args)
    {
      if (!(this instanceof WrappedRTCPeerConnection))
        return RealRTCPeerConnection();

      let configuration = protectConfiguration(args[0]);

      // Dado que el constructor antiguo de webkitRTCPeerConnection toma un argumento
      // opcional de segundo parámetro, debemos asegurarnos de pasarlo. Necesario
      // para versiones antiguas de Chrome como la 51.
      let constraints = undefined;
      if (args.length > 1)
        constraints = args[1];

      let peerconnection = new RealRTCPeerConnection(configuration,
                                                     constraints);
      checkConfiguration(peerconnection, configuration);
      return peerconnection;
    };

    WrappedRTCPeerConnection.prototype = RealRTCPeerConnection.prototype;

    let boundWrappedRTCPeerConnection = WrappedRTCPeerConnection.bind();
    copyProperties(RealRTCPeerConnection, boundWrappedRTCPeerConnection,
                   ["generateCertificate", "name", "prototype"]);
    RealRTCPeerConnection.prototype.constructor = boundWrappedRTCPeerConnection;

    if ("RTCPeerConnection" in window)
      window.RTCPeerConnection = boundWrappedRTCPeerConnection;
    if ("webkitRTCPeerConnection" in window)
      window.webkitRTCPeerConnection = boundWrappedRTCPeerConnection;
  }
})('abp-request-aic05wltexc', true);. 468b7929-46c3-4249-b516-189e58962157:27:22

Otro CSP:

Content Security Policy: La configuración de la página bloqueó la carga de un recurso en línea ("script-src"). Origen: try { if (typeof Navigator.prototype.sendBeacon === 'function') { Navigator.prototype.sendBeacon = function(url, data) { return true; }; } } catch (exception) { console.error(exception); }. script.js:517:22

Y uno más (el enlace aquí ya fue agregado, por cierto)

Content Security Policy: La configuración de la página bloqueó la carga de un recurso en https://widget-v4.tidiochat.com//1_23_3/static/js/widget.a6a6e2b4c2401b7c523f.js ("script-src"). xgahvvrt0kwvb7p6crbxuolt4omnin1u.js:1:12450

¿Blanca listaste esa URL específica? Los mensajes de error suelen ser claros sobre qué URLs deben ser añadidas a la lista blanca.

1 me gusta

Gracias, Jeff. Lo estaba tomando demasiado literalmente y usando toda la URL de https://widget-v4.tidiochat.com//1_23_3/static/js/widget.a6a6e2b4c2401b7c523f.js

Ahora sí funciona. Todavía estoy recibiendo la primera CSP; si está funcionando, ¿supongo que es seguro ignorarla?

1 me gusta

Si recibes errores de CSP en la consola F12, debes agregar ese dominio del error a la lista blanca.

Me refería al primero, que no tiene una URL; más bien es una advertencia sobre problemas en Chrome con el envoltorio del contexto del marco. Curiosamente, aparece en Firefox pero no en Chrome, así que creo que simplemente lo ignoraré, ya que parece funcionar correctamente. Agradezco tu ayuda :+1:

Nunca he probado este chat en particular

Probé LiveChat ayer y funciona sin errores

2 Me gusta

Hola a todos,

¡Gracias por la excelente guía! Seguí las instrucciones para agregar una encuesta de retroalimentación, pero HubSpot sigue cambiando el enlace de análisis (js.hs-analytics/), por lo que tengo que agregar constantemente nuevos enlaces a la lista de permitidos.

La única solución que encontré que funciona de manera consistente es desactivar por completo la política, pero no parece muy segura.

¿Alguna idea?

Estoy viendo tu sitio ahora mismo, pero no puedo encontrar el código de la encuesta que ingresaste en Personalizar > Temas

Edición: Debería estar solucionado ahora, he vuelto a activar la CSP. Avísame si tienes problemas.

1 me gusta

¡Hola @Dax!

¡Gracias! ¿Podrías decirme cómo lo solucionaste y cuál fue el problema?

Edición: Veo que añadiste un comodín, ¡genial! Creo que lo mejor sería agregar un comentario debajo de ese cuadro, para que sepamos que los comodines también son una opción. Yo pensé en una solución así, pero yo, tonto de mí, no la probé.

¡Gracias!

1 me gusta

Tengo curiosidad, ¿cuál es el valor de conectar un CRM como HubSpot a nuestro sitio de la comunidad Discourse?