Настройка интеграции чата HubSpot

Хотите интегрировать чат-бота HubSpot в Discourse? Давайте разберёмся, как это сделать!

  1. Создайте аккаунт на HubSpot

  2. Выберите раздел «Чат»

  3. Настройте интерфейс и время работы по своему усмотрению


  4. Скопируйте код

  5. Создайте новый компонент темы и вставьте код во вкладку Common - </body>. Добавьте новый компонент в вашу основную тему (или темы).

  6. Завершите проверку в HubSpot, чтобы активировать виджет, и перейдите на свой сайт

  7. Готово :tada:

12 лайков

Привет, Даниэлла, я пробовал эту интеграцию с тремя разными провайдерами чата, и после добавления всех найденных ссылок в security policy script src получаю одну и ту же ошибку CSP. В данном случае речь идёт о Tidio, но то же самое произошло с LiveChat и Pure Chat. Есть какие-то идеи, что может происходить?

Content Security Policy: Настройки страницы заблокировали загрузку ресурса по адресу eval ("script-src"). Источник: (function injected(eventName, injectedIntoContentWindow)
{
  let checkRequest;

  /*
   * Обёртка контекста фрейма
   *
   * В некоторых крайних случаях Chrome не запускает контентные скрипты внутри фреймов.
   * Сайты начали злоупотреблять этим фактом для доступа к незащищённым API через
   * contentWindow фрейма (#4586, 5207). Поэтому, пока Chrome не будет последовательно
   * запускать контентные скрипты для всех фреймов, мы должны следить за тем, чтобы
   * (пере)внедрять наши обёртки при доступе к 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"
    );

    // Похоже, что в HTMLObjectElement.prototype.contentWindow не существует
    // в старых версиях Chrome, таких как 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);
  }

  /*
   * Обёртка RTCPeerConnection
   *
   * WebRTC API в Chrome пока не позволяет блокировать
   * соединения WebRTC.
   * См. https://bugs.chromium.org/p/chromium/issues/detail?id=707683
   */
  let RealCustomEvent = window.CustomEvent;

  // Если мы были внедрены во фрейм через contentWindow, то мы можем просто
  // взять копию checkRequest, оставленную для нас родительским документом. В противном
  // случае нам нужно настроить её сейчас, вместе с функциями обработки событий.
  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}}));
    };
  }

  // Вызывается только до кода страницы, не защищено.
  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 есть опция (media.peerconnection.enabled) для отключения WebRTC,
  // в этом случае RealRTCPeerConnection не определён.
  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;
    };

    // Было бы гораздо проще использовать метод .getConfiguration для получения
    // нормализованной и безопасной конфигурации из экземпляра RTCPeerConnection.
    // К сожалению, он не реализован в нестабильной версии Chrome 59.
    // См. 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 не перебирает псевдомассивы 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)
        {
          // Вызов .close() выбрасывает ошибку, если соединение уже закрыто.
          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 (протестирована с 59) уже реализовала
    // setConfiguration, поэтому нам нужно обернуть и его, если он существует.
    // 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);

        // Сначала вызовем настоящий метод, чтобы он проверил конфигурацию за нас.
        // Кроме того, checkRequest всё равно асинхронный.
        realSetConfiguration(this, configuration);
        checkConfiguration(this, configuration);
      };
    }

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

      let configuration = protectConfiguration(args[0]);

      // Поскольку старый конструктор webkitRTCPeerConnection принимает необязательный
      // второй аргумент, нам нужно убедиться, что мы передаём его. Это необходимо
      // для старых версий Chrome, таких как 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

Ещё одна ошибка CSP:

Content Security Policy: Настройки страницы заблокировали загрузку ресурса по адресу inline ("script-src"). Источник: try { if (typeof Navigator.prototype.sendBeacon === 'function') { Navigator.prototype.sendBeacon = function(url, data) { return true; }; } } catch (exception) { console.error(exception); }. script.js:517:22

И ещё одна (ссылка здесь уже добавлена, к сведению):

Content Security Policy: Настройки страницы заблокировали загрузку ресурса по адресу https://widget-v4.tidiochat.com//1_23_3/static/js/widget.a6a6e2b4c2401b7c523f.js ("script-src"). xgahvvrt0kwvb7p6crbxuolt4omnin1u.js:1:12450

Вы добавили этот конкретный URL в белый список? Сообщения об ошибках обычно четко указывают, какие URL-адреса необходимо добавить в белый список.

1 лайк

Спасибо, Джефф. Я воспринял это слишком буквально и использовал полный URL https://widget-v4.tidiochat.com//1_23_3/static/js/widget.a6a6e2b4c2401b7c523f.js.

Теперь всё работает. У меня всё ещё появляется первое предупреждение CSP. Если всё функционирует, можно ли считать, что его можно игнорировать?

1 лайк

Если в консоли F12 появляются ошибки CSP, добавьте домен из сообщения об ошибке в белый список.

Я имел в виду первый вариант, у которого нет URL, — скорее предупреждение о проблемах в Chrome с обёрткой контекста фрейма. Странно, что оно отображается в Firefox, но не в Chrome, поэтому я думаю, что просто проигнорирую его, так как всё работает нормально. Спасибо за вашу помощь :+1:

Я никогда не пробовал этот конкретный чат

Я тестировал LiveChat вчера, и он работает без ошибок

2 лайка

Всем привет,

Спасибо за отличный гайд! Я следовал инструкции, чтобы добавить опрос для сбора обратной связи, но HubSpot постоянно меняет ссылку для аналитики (js.hs-analytics/), из-за чего мне приходится постоянно добавлять новые ссылки в белый список.

Единственное решение, которое, как я обнаружил, стабильно работает, — это полностью отключить политику, но это кажется не слишком безопасным.

Есть какие-то идеи?

Я сейчас просматриваю ваш сайт, но не могу найти код опроса, который вы ввели в разделе Настроить > Темы.

Редактирование: Теперь всё должно быть исправлено, я снова включил CSP. Дайте знать, если возникнут проблемы.

1 лайк

Привет, @Dax,

Спасибо! Можешь, пожалуйста, рассказать, как ты это исправил и в чём была проблема?

Редактирование: Вижу, что ты добавил wildcard (шаблонный символ), отлично! Думаю, было бы лучше добавить комментарий под этим полем, чтобы мы знали, что также можно использовать wildcard-символы! Я думал о таком решении, но, глупый я, не попробовал его применить.

Спасибо!

1 лайк

Мне интересно, какова ценность подключения CRM, такой как HubSpot, к нашему сайту сообщества Discourse?