使用 Pub/Sub 构建在防火墙后的 Google Chat 应用

本页面介绍如何使用 Pub/Sub 创建聊天应用。如果贵组织设有可阻止 Chat 向 Chat 应用发送消息的防火墙,或者 Chat 应用使用 Google Workspace Events API,则此类聊天应用架构非常有用。但是,由于这些 Chat 应用只能发送和接收异步消息,因此该架构存在以下限制:

  • 无法在消息中使用对话框。请改用卡片消息
  • 无法使用同步响应更新个别卡片。请改为通过调用 patch 方法来更新整条消息。

下图显示了使用 Pub/Sub 构建的 Chat 应用的架构:

使用 Pub/Sub 实现的聊天应用的架构。

在上图中,与 Pub/Sub Chat 应用交互的用户具有以下信息流:

  1. 用户在 Chat 中通过私信或 Chat 聊天室向 Chat 应用发送消息,或者事件发生在 Chat 聊天室具有有效订阅的 Chat 聊天室中。

  2. Chat 将消息发送到 Pub/Sub 主题。

  3. 应用服务器是包含 Chat 应用逻辑的云端或本地系统,它会订阅 Pub/Sub 主题,以便通过防火墙接收消息。

  4. (可选)Chat 应用可以调用 Chat API 来异步发布消息或执行其他操作。

前提条件

Java

设置环境

在使用 Google API 之前,您需要在 Google Cloud 项目中启用它们。您可以在单个 Google Cloud 项目中启用一个或多个 API。
  • 在 Google Cloud 控制台中,启用 Google Chat API 和 Pub/Sub API。

    启用 API

设置 Pub/Sub

  1. 创建 Pub/Sub 主题,Chat API 可向其发送消息。我们建议您为每个 Chat 应用使用一个主题。

  2. 通过将 Pub/Sub Publisher 角色分配给以下服务帐号,向 Chat 授予发布权限

    chat-api-push@system.gserviceaccount.com
    
  3. 为 Chat 应用创建服务帐号,以使用 Pub/Sub 和 Chat 进行授权,并将私钥文件保存到您的工作目录中。

  4. 为该主题创建拉取订阅

  5. 为您之前创建的服务帐号分配订阅的 Pub/Sub Subscriber 角色

编写脚本

Java

  1. 在 CLI 中,提供服务账号凭据

    export GOOGLE_APPLICATION_CREDENTIALS=SERVICE_ACCOUNT_FILE_PATH
    
  2. 在您的工作目录中,创建一个名为 pom.xml 的文件。

  3. pom.xml 文件中,粘贴以下代码:

    <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    
    <groupId>com.google.chat.pubsub</groupId>
    <artifactId>java-pubsub-app</artifactId>
    <version>0.1.0</version>
    
    <name>java-pubsub-app</name>
    
    <properties>
      <maven.compiler.target>11</maven.compiler.target>
      <maven.compiler.source>11</maven.compiler.source>
    </properties>
    
    <dependencyManagement>
      <dependencies>
        <dependency>
          <groupId>com.google.cloud</groupId>
          <artifactId>libraries-bom</artifactId>
          <version>26.26.0</version>
          <type>pom</type>
          <scope>import</scope>
        </dependency>
      </dependencies>
    </dependencyManagement>
    
    <dependencies>
      <dependency>
        <groupId>com.google.code.gson</groupId>
        <artifactId>gson</artifactId>
        <version>2.9.1</version>
      </dependency>
      <dependency>
        <groupId>com.google.api-client</groupId>
        <artifactId>google-api-client</artifactId>
        <version>1.32.1</version>
      </dependency>
      <dependency>
        <groupId>com.google.cloud</groupId>
        <artifactId>google-cloud-pubsub</artifactId>
      </dependency>
      <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
        <version>2.14.2</version>
      </dependency>
    </dependencies>
    
    <build>
      <pluginManagement>
        <plugins>
          <plugin>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>3.8.0</version>
          </plugin>
        </plugins>
      </pluginManagement>
    </build>
    </project>
    
  4. 在工作目录中,创建目录结构 src/main/java

  5. src/main/java 目录中,创建一个名为 Main.java 的文件。

  6. Main.java 中,粘贴以下代码:

    import com.fasterxml.jackson.databind.JsonNode;
    import com.fasterxml.jackson.databind.ObjectMapper;
    import com.fasterxml.jackson.databind.node.JsonNodeFactory;
    import com.fasterxml.jackson.databind.node.ObjectNode;
    import com.google.api.client.googleapis.auth.oauth2.GoogleCredential;
    import com.google.api.client.googleapis.javanet.GoogleNetHttpTransport;
    import com.google.api.client.http.ByteArrayContent;
    import com.google.api.client.http.GenericUrl;
    import com.google.api.client.http.HttpContent;
    import com.google.api.client.http.HttpRequest;
    import com.google.api.client.http.HttpRequestFactory;
    import com.google.api.client.http.HttpTransport;
    import com.google.cloud.pubsub.v1.AckReplyConsumer;
    import com.google.cloud.pubsub.v1.MessageReceiver;
    import com.google.cloud.pubsub.v1.Subscriber;
    import com.google.pubsub.v1.PubsubMessage;
    import com.google.pubsub.v1.ProjectSubscriptionName;
    import java.io.FileInputStream;
    import java.util.Collections;
    
    public class Main {
    
      public static final String CREDENTIALS_PATH_ENV_PROPERTY = "GOOGLE_APPLICATION_CREDENTIALS";
    
      // Google Cloud Project ID
      public static final String PROJECT_ID = PROJECT_ID;
    
      // Cloud Pub/Sub Subscription ID
      public static final String SUBSCRIPTION_ID = SUBSCRIPTION_ID
    
      public static void main(String[] args) throws Exception {
        ProjectSubscriptionName subscriptionName =
            ProjectSubscriptionName.of(PROJECT_ID, SUBSCRIPTION_ID);
    
        // Instantiate app, which implements an asynchronous message receiver.
        EchoApp echoApp = new EchoApp();
    
        // Create a subscriber for <var>SUBSCRIPTION_ID</var> bound to the message receiver
        final Subscriber subscriber =
            Subscriber.newBuilder(subscriptionName, echoApp).build();
        System.out.println("Starting subscriber...");
        subscriber.startAsync();
    
        // Wait for termination
        subscriber.awaitTerminated();
      }
    }
    
    / **
    * A demo app which implements {@link MessageReceiver} to receive messages. It simply echoes the
    * incoming messages.
    */
    class EchoApp implements MessageReceiver {
    
      // Path to the private key JSON file of the service account to be used for posting response
      // messages to Google Chat.
      // In this demo, we are using the same service account for authorizing with Cloud Pub/Sub to
      // receive messages and authorizing with Google Chat to post messages. If you are using
      // different service accounts, please set the path to the private key JSON file of the service
      // account used to post messages to Google Chat here.
      private static final String SERVICE_ACCOUNT_KEY_PATH =
          System.getenv(Main.CREDENTIALS_PATH_ENV_PROPERTY);
    
      // Developer code for Google Chat API scope.
      private static final String GOOGLE_CHAT_API_SCOPE = "https://www.googleapis.com/auth/chat.bot";
    
      // Response URL Template with placeholders for space id.
      private static final String RESPONSE_URL_TEMPLATE =
          "https://chat.googleapis.com/v1/__SPACE_ID__/messages";
    
      // Response echo message template.
      private static final String RESPONSE_TEMPLATE = "You said: `__MESSAGE__`";
    
      private static final String ADDED_RESPONSE = "Thank you for adding me!";
    
      GoogleCredential credential;
      HttpTransport httpTransport;
      HttpRequestFactory requestFactory;
    
      EchoApp() throws Exception {
        credential =
            GoogleCredential.fromStream(new FileInputStream(SERVICE_ACCOUNT_KEY_PATH))
                .createScoped(Collections.singleton(GOOGLE_CHAT_API_SCOPE));
        httpTransport = GoogleNetHttpTransport.newTrustedTransport();
        requestFactory = httpTransport.createRequestFactory(credential);
      }
    
      // Called when a message is received by the subscriber.
      @Override
      public void receiveMessage(PubsubMessage pubsubMessage, AckReplyConsumer consumer) {
        System.out.println("Id : " + pubsubMessage.getMessageId());
        // handle incoming message, then ack/nack the received message
        try {
          ObjectMapper mapper = new ObjectMapper();
          JsonNode dataJson = mapper.readTree(pubsubMessage.getData().toStringUtf8());
          System.out.println("Data : " + dataJson.toString());
          handle(dataJson);
          consumer.ack();
        } catch (Exception e) {
          System.out.println(e);
          consumer.nack();
        }
      }
    
      public void handle(JsonNode eventJson) throws Exception {
        JsonNodeFactory jsonNodeFactory = new JsonNodeFactory(false);
        ObjectNode responseNode = jsonNodeFactory.objectNode();
    
        // Construct the response depending on the event received.
    
        String eventType = eventJson.get("type").asText();
        switch (eventType) {
          case "ADDED_TO_SPACE":
            responseNode.put("text", ADDED_RESPONSE);
            // An app can also be added to a space by @mentioning it in a message. In that case, we fall
            // through to the MESSAGE case and let the app respond. If the app was added using the
            // invite flow, we just post a thank you message in the space.
            if(!eventJson.has("message")) {
              break;
            }
          case "MESSAGE":
            responseNode.put("text",
                RESPONSE_TEMPLATE.replaceFirst(
                    "__MESSAGE__", eventJson.get("message").get("text").asText()));
            // In case of message, post the response in the same thread.
            ObjectNode threadNode = jsonNodeFactory.objectNode();
            threadNode.put("name", eventJson.get("message").get("thread").get("name").asText());
            responseNode.put("thread", threadNode);
            break;
          case "REMOVED_FROM_SPACE":
          default:
            // Do nothing
            return;
        }
    
        // Post the response to Google Chat.
    
        String URI =
            RESPONSE_URL_TEMPLATE.replaceFirst(
                "__SPACE_ID__", eventJson.get("space").get("name").asText());
        GenericUrl url = new GenericUrl(URI);
    
        HttpContent content =
            new ByteArrayContent("application/json", responseNode.toString().getBytes("UTF-8"));
        HttpRequest request = requestFactory.buildPostRequest(url, content);
        com.google.api.client.http.HttpResponse response = request.execute();
      }
    }
    

    替换以下内容:

    • PROJECT_ID:Google Cloud 项目 ID。
    • SUBSCRIPTION_ID:您之前创建的 Pub/Sub 订阅的订阅 ID。

将应用发布到 Chat

  1. 在 Google Cloud 控制台中,依次点击“菜单”图标 > API 和服务 > 已启用的 API 和服务 > Google Chat API > 配置

    前往“配置”

  2. 为 Pub/Sub 配置 Chat 应用:

    1. 应用名称中,输入 Quickstart App
    2. 头像网址中输入 https://developers.google.com/chat/images/quickstart-app-avatar.png
    3. 说明中,输入 Quickstart app
    4. 功能下方,选择接收 1 对 1 消息加入聊天室和群组对话
    5. 连接设置下,选择 Cloud Pub/Sub 并粘贴您之前创建的 Pub/Sub 主题的名称。
    6. 公开范围下方,选择将此 Google Chat 应用提供给您网域中的特定人员和群组,然后输入您的电子邮件地址。
    7. 日志下,选择将错误记录到 Logging
  3. 点击保存

应用已准备好在 Chat 中接收和回复消息。

运行脚本

在 CLI 中,切换到您的工作目录并运行以下脚本:

Java

mvn compile exec:java -Dexec.mainClass=Main

运行该代码时,应用会开始监听发布到 Pub/Sub 主题的消息。

测试 Chat 应用

如需测试您的 Chat 应用,请向该应用发送私信:

  1. 打开 Google Chat
  2. 如需向应用发送私信,请点击“开始聊天”图标 ,然后在显示的窗口中点击查找应用
  3. Find apps 对话框中,搜索“quickstart App”。
  4. 如需打开与应用的私信,请找到 quickstart App,然后点击 Add > > Chat
  5. 在私信中输入 Hello,然后按 enter。Chat 应用会回显该消息。

如需添加可信测试员并详细了解如何测试交互功能,请参阅测试 Google Chat 应用的交互功能

问题排查

当 Google Chat 应用或卡片返回错误时,Chat 界面会显示“出了点问题”或“无法处理您的请求”的消息。有时,Chat 界面不会显示任何错误消息,但 Chat 应用或卡片会产生意外结果;例如,卡片消息可能不会显示。

虽然 Chat 界面中可能不会显示错误消息,但当为 Chat 应用启用错误日志记录功能时,您可以借助描述性错误消息和日志数据修正错误。如需查看、调试和修正错误方面的帮助,请参阅排查并修正 Google Chat 错误

清理

为避免因本教程中使用的资源导致您的 Google Cloud 帐号产生费用,我们建议您删除该 Cloud 项目。

  1. 在 Google Cloud 控制台中,前往管理资源页面。依次点击菜单图标 > IAM 和管理 > 管理资源

    前往 Resource Manager

  2. 在项目列表中,选择要删除的项目,然后点击删除
  3. 在对话框中输入项目 ID,然后点击关停以删除项目。