设置 HubSpot 聊天集成

您想在 Discourse 中集成 CSM 吗?让我们看看如何在 Discourse 中集成 HubSpot 聊天!

  1. HubSpot 创建一个账户

  2. 选择“聊天”

  3. 根据需要自定义界面和可用性


  4. 复制代码

  5. 创建一个新的主题组件并将其粘贴到 Common - </body> 选项卡中。将新组件添加到您的主主题中

  6. 在 Hubspot 上完成验证以激活小部件并访问您的网站

  7. 完成 :tada:

12 个赞

你好,Daniella,

我尝试将这项集成与三家不同的聊天提供商对接,但在将我能找到的所有链接添加到 security policy script src 后,仍然收到相同的 CSP 错误。这个特定问题出在 Tidio 上,但 LiveChat 和 Pure Chat 也出现了同样的情况。您有什么想法吗?

Content Security Policy: 页面设置阻止了在 eval 处加载资源("script-src")。来源:(function injected(eventName, injectedIntoContentWindow)
{
  let checkRequest;

  /*
   * Frame 上下文包装器
   *
   * 在某些边缘情况下,Chrome 不会在 frame 内部运行内容脚本。
   * 一些网站开始利用这一点,通过 frame 的 contentWindow 访问未包装的 API(#4586, 5207)。
   * 因此,在 Chrome 能够一致地为所有 frame 运行内容脚本之前,我们必须确保在访问 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"
    );

    // 显然,在旧版 Chrome(如 51)中,HTMLObjectElement.prototype.contentWindow 不存在。
    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 包装器
   *
   * Chrome 中的 webRequest API 目前尚不允许阻止 WebRTC 连接。
   * 参见 https://bugs.chromium.org/p/chromium/issues/detail?id=707683
   */
  let RealCustomEvent = window.CustomEvent;

  // 如果我们是通过 contentWindow 注入到 frame 中的,那么我们可以直接获取父文档留给我们的 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 为 undefined。
  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: 页面设置阻止了内联资源的加载("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 个赞

谢谢 Jeff,我之前理解得太字面了,直接使用了整个 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

谢谢!能否告诉我你是如何解决的,以及问题出在哪里?

编辑:我看到你添加了一个通配符,太棒了!我觉得最好在那个框下面加个注释,这样我们就知道通配符也是一个可选方案了!我其实也想过类似的解决方案,但我太笨了,没去尝试一下。

谢谢!

1 个赞

我很好奇,将 HubSpot 等 CRM 连接到我们的 Discourse 社区站点有什么价值?