使用 Pub/Sub 建立設有防火牆的 Google Chat 應用程式

本頁面將說明如何使用 Pub/Sub。這類 Chat 應用程式採用的架構 可能會使 Chat 無法 正在傳送訊息到 Chat 應用程式 Chat 應用程式會使用 Google Workspace 事件 API。不過, 因為這類架構具有下列限制 即時通訊應用程式只能收發訊息 非同步郵件

  • 無法使用對話方塊 訊息功能。而是改用 資訊卡訊息
  • 無法以同步回應更新個別資訊卡。請改為更新 方法是呼叫 patch敬上 方法。

下圖顯示 使用 Pub/Sub 建構的即時通訊應用程式:

透過 Pub/Sub 實作的 Chat 應用程式架構。

在上圖中,使用者與 Pub/Sub 互動 Chat 應用程式提供以下資訊流:

  1. 使用者在 Chat 中傳送訊息給 透過即時訊息或 Chat 聊天室或 Chat 聊天室中活動 處於使用中狀態的應用程式 subscription

  2. Chat 會將訊息傳送至 Pub/Sub 主題。

  3. 應用程式伺服器,可以是雲端或地端部署系統 包含 Chat 應用程式邏輯,訂閱 Pub/Sub 主題,以便透過防火牆接收訊息。

  4. Chat 應用程式也可以視需要呼叫 透過 Chat API 以非同步方式發布訊息或執行其他 作業。

必要條件

Java

Python

Node.js

設定環境

您必須先在 Google Cloud 專案中啟用這些 Google API,才能使用這些 API。 您可以在單一 Google Cloud 專案中啟用一或多個 API。
  • 在 Google Cloud 控制台中,啟用 Google Chat API 和 Pub/Sub API。

    啟用 API

設定 Pub/Sub

  1. 建立 Pub/Sub 主題 方便 Chat API 傳送訊息建議您使用 單一主題。

  2. 授予 Chat 發布權限Pub/Sub 發布者角色指派給以下項目,藉此建立主題 服務帳戶:

    chat-api-push@system.gserviceaccount.com
    
  3. 建立服務帳戶 給 Chat 應用程式使用 Pub/Sub 授權 進行即時通訊,並將私密金鑰檔案儲存至工作目錄。

  4. 建立提取訂閱項目 新增至主題。

  5. 為訂閱項目指派 Pub/Sub 訂閱者角色 為先前建立的服務帳戶啟用服務帳戶

編寫指令碼

Java

  1. 在 CLI 中,提供服務帳戶憑證

    export GOOGLE_APPLICATION_CREDENTIALS=SERVICE_ACCOUNT_FILE_PATH
    
  2. 在 CLI 中,提供 Google Cloud 專案 ID:

    export PROJECT_ID=PROJECT_ID
    
  3. 在 CLI 中,請提供 Pub/Sub 訂閱項目的訂閱項目 ID, 先前建立的項目

    export SUBSCRIPTION_ID=SUBSCRIPTION_ID
    
  4. 在工作目錄中,建立名為 pom.xml 的檔案。

  5. pom.xml 檔案中,貼上下列程式碼:

    java/pub-sub-app/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</groupId>
      <artifactId>pub-sub-app</artifactId>
      <version>0.1.0</version>
    
      <name>pub-sub-app-java</name>
    
      <properties>
        <maven.compiler.release>21</maven.compiler.release>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
      </properties>
    
      <dependencies>
        <!-- Google Chat GAPIC library -->
        <dependency>
          <groupId>com.google.api.grpc</groupId>
          <artifactId>proto-google-cloud-chat-v1</artifactId>
          <version>0.8.0</version>
        </dependency>
        <dependency>
          <groupId>com.google.api</groupId>
          <artifactId>gax</artifactId>
          <version>2.48.1</version>
        </dependency>
        <dependency>
          <groupId>com.google.cloud</groupId>
          <artifactId>google-cloud-chat</artifactId>
          <version>0.1.0</version>
        </dependency>
        <!-- Google Cloud Pub/Sub library -->
        <dependency>
          <groupId>com.google.cloud</groupId>
          <artifactId>google-cloud-pubsub</artifactId>
        <version>1.125.8</version>
        </dependency>
        <!-- JSON utilities -->
        <dependency>
          <groupId>com.fasterxml.jackson.core</groupId>
          <artifactId>jackson-databind</artifactId>
          <version>2.14.2</version>
        </dependency>
      </dependencies>
    
    </project>
  6. 在工作目錄中,建立目錄結構 src/main/java

  7. src/main/java 目錄中,建立名為 Main.java 的檔案。

  8. Main.java 中,貼上下列程式碼:

    java/pub-sub-app/src/main/java/Main.java
    import com.fasterxml.jackson.databind.JsonNode;
    import com.fasterxml.jackson.databind.ObjectMapper;
    import com.google.api.gax.core.FixedCredentialsProvider;
    import com.google.auth.oauth2.GoogleCredentials;
    import com.google.chat.v1.ChatServiceClient;
    import com.google.chat.v1.ChatServiceSettings;
    import com.google.chat.v1.CreateMessageRequest;
    import com.google.chat.v1.CreateMessageRequest.MessageReplyOption;
    import com.google.chat.v1.Message;
    import com.google.chat.v1.Thread;
    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.ProjectSubscriptionName;
    import com.google.pubsub.v1.PubsubMessage;
    import java.io.FileInputStream;
    import java.util.Collections;
    
    public class Main {
    
      public static final String PROJECT_ID_ENV_PROPERTY = "PROJECT_ID";
      public static final String SUBSCRIPTION_ID_ENV_PROPERTY = "SUBSCRIPTION_ID";
      public static final String CREDENTIALS_PATH_ENV_PROPERTY = "GOOGLE_APPLICATION_CREDENTIALS";
    
      public static void main(String[] args) throws Exception {
        ProjectSubscriptionName subscriptionName =
            ProjectSubscriptionName.of(
                System.getenv(Main.PROJECT_ID_ENV_PROPERTY),
                System.getenv(Main.SUBSCRIPTION_ID_ENV_PROPERTY));
    
        // 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("Subscriber is listening to events...");
        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";
    
      private static final String ADDED_RESPONSE = "Thank you for adding me!";
    
      ChatServiceClient chatServiceClient;
    
      EchoApp() throws Exception {
        GoogleCredentials credential =
            GoogleCredentials.fromStream(new FileInputStream(SERVICE_ACCOUNT_KEY_PATH))
                .createScoped(Collections.singleton(GOOGLE_CHAT_API_SCOPE));
    
        // Create the ChatServiceSettings with the app credentials
        ChatServiceSettings chatServiceSettings =
            ChatServiceSettings.newBuilder()
                .setCredentialsProvider(FixedCredentialsProvider.create(credential))
                .build();
    
        // Set the Chat service client
        chatServiceClient = ChatServiceClient.create(chatServiceSettings);
      }
    
      // 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();
        }
      }
    
      // Send message to Google Chat based on the type of event.
      public void handle(JsonNode eventJson) throws Exception {
        CreateMessageRequest createMessageRequest;
        switch (eventJson.get("type").asText()) {
          case "ADDED_TO_SPACE":
            // 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")) {
              createMessageRequest =
                  CreateMessageRequest.newBuilder()
                      .setParent(eventJson.get("space").get("name").asText())
                      .setMessage(Message.newBuilder().setText(ADDED_RESPONSE).build())
                      .build();
              break;
            }
          case "MESSAGE":
            // In case of message, post the response in the same thread.
            createMessageRequest =
                CreateMessageRequest.newBuilder()
                    .setParent(eventJson.get("space").get("name").asText())
                    .setMessageReplyOption(MessageReplyOption.REPLY_MESSAGE_FALLBACK_TO_NEW_THREAD)
                    .setMessage(
                        Message.newBuilder()
                            .setText(
                                "You said: `" + eventJson.get("message").get("text").asText() + "`")
                            .setThread(
                                Thread.newBuilder()
                                    .setName(
                                        eventJson.get("message").get("thread").get("name").asText())
                                    .build())
                            .build())
                    .build();
            break;
          case "REMOVED_FROM_SPACE":
          default:
            // Do nothing
            return;
        }
    
        // Post the response to Google Chat.
        chatServiceClient.createMessage(createMessageRequest);
      }
    }

Python

  1. 在 CLI 中,提供服務帳戶憑證

    export GOOGLE_APPLICATION_CREDENTIALS=SERVICE_ACCOUNT_FILE_PATH
    
  2. 在 CLI 中,提供 Google Cloud 專案 ID:

    export PROJECT_ID=PROJECT_ID
    
  3. 在 CLI 中,請提供 Pub/Sub 訂閱項目的訂閱項目 ID, 先前建立的項目

    export SUBSCRIPTION_ID=SUBSCRIPTION_ID
    
  4. 在工作目錄中,建立名為 requirements.txt 的檔案。

  5. requirements.txt 檔案中,貼上下列程式碼:

    python/pub-sub-app/requirements.txt
    google-cloud-pubsub>=2.23.0
    google-apps-chat==0.1.9
    
  6. 在工作目錄中,建立名為 app.py 的檔案。

  7. app.py 中,貼上下列程式碼:

    python/pub-sub-app/app.py
    import json
    import logging
    import os
    import sys
    import time
    from google.apps import chat_v1 as google_chat
    from google.cloud import pubsub_v1
    from google.oauth2.service_account import Credentials
    
    
    def receive_messages():
      """Receives messages from a pull subscription."""
    
      scopes = ['https://www.googleapis.com/auth/chat.bot']
      service_account_key_path = os.environ.get(
        'GOOGLE_APPLICATION_CREDENTIALS')
      creds = Credentials.from_service_account_file(
        service_account_key_path)
      chat = google_chat.ChatServiceClient(
        credentials = creds,
        client_options = {
          "scopes": scopes
        })
    
      project_id = os.environ.get('PROJECT_ID')
      subscription_id = os.environ.get('SUBSCRIPTION_ID')
      subscriber = pubsub_v1.SubscriberClient()
      subscription_path = subscriber.subscription_path(
          project_id, subscription_id)
    
      # Handle incoming message, then ack/nack the received message
      def callback(message):
        event = json.loads(message.data)
        logging.info('Data : %s', event)
        space_name = event['space']['name']
    
        # Post the response to Google Chat.
        request = format_request(event)
        if request is not None:
          chat.create_message(request)
    
        # Ack the message.
        message.ack()
    
      subscriber.subscribe(subscription_path, callback = callback)
      logging.info('Listening for messages on %s', subscription_path)
    
      # Keep main thread from exiting while waiting for messages
      while True:
        time.sleep(60)
    
    
    def format_request(event):
      """Send message to Google Chat based on the type of event.
      Args:
        event: A dictionary with the event data.
      """
      space_name = event['space']['name']
      event_type = event['type']
    
      # If the app was removed, we don't respond.
      if event['type'] == 'REMOVED_FROM_SPACE':
        logging.info('App removed rom space %s', space_name)
        return
      elif event_type == 'ADDED_TO_SPACE' and 'message' not in event:
        # 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.
        return google_chat.CreateMessageRequest(
            parent = space_name,
            message = {
              'text': 'Thank you for adding me!'
            }
        )
      elif event_type in ['ADDED_TO_SPACE', 'MESSAGE']:
        # In case of message, post the response in the same thread.
        return google_chat.CreateMessageRequest(
            parent = space_name,
            message_reply_option = google_chat.CreateMessageRequest.MessageReplyOption.REPLY_MESSAGE_FALLBACK_TO_NEW_THREAD,
            message = {
              'text': 'You said: `' + event['message']['text'] + '`',
              'thread': {
                'name': event['message']['thread']['name']
              }
            }
        )
    
    
    if __name__ == '__main__':
      if 'PROJECT_ID' not in os.environ:
        logging.error('Missing PROJECT_ID env var.')
        sys.exit(1)
    
      if 'SUBSCRIPTION_ID' not in os.environ:
        logging.error('Missing SUBSCRIPTION_ID env var.')
        sys.exit(1)
    
      if 'GOOGLE_APPLICATION_CREDENTIALS' not in os.environ:
        logging.error('Missing GOOGLE_APPLICATION_CREDENTIALS env var.')
        sys.exit(1)
    
      logging.basicConfig(
          level=logging.INFO,
          style='{',
          format='{levelname:.1}{asctime} {filename}:{lineno}] {message}')
      receive_messages()
    

Node.js

  1. 在 CLI 中,提供服務帳戶憑證

    export GOOGLE_APPLICATION_CREDENTIALS=SERVICE_ACCOUNT_FILE_PATH
    
  2. 在 CLI 中,提供 Google Cloud 專案 ID:

    export PROJECT_ID=PROJECT_ID
    
  3. 在 CLI 中,請提供 Pub/Sub 訂閱項目的訂閱項目 ID, 先前建立的項目

    export SUBSCRIPTION_ID=SUBSCRIPTION_ID
    
  4. 在工作目錄中,建立名為 package.json 的檔案。

  5. package.json 檔案中,貼上下列程式碼:

    node/pub-sub-app/package.json
    {
      "name": "pub-sub-app",
      "version": "1.0.0",
      "description": "Google Chat App that listens for messages via Cloud Pub/Sub",
      "main": "index.js",
      "scripts": {
        "start": "node index.js",
        "test": "echo \"Error: no test specified\" && exit 1"
      },
      "dependencies": {
        "@google-apps/chat": "^0.4.0",
        "@google-cloud/pubsub": "^4.5.0"
      },
      "license": "Apache-2.0"
    }
    
  6. 在工作目錄中,建立名為 index.js 的檔案。

  7. index.js 中,貼上下列程式碼:

    node/pub-sub-app/index.js
    const {ChatServiceClient} = require('@google-apps/chat');
    const {MessageReplyOption} = require('@google-apps/chat').protos.google.chat.v1.CreateMessageRequest;
    const {PubSub} = require('@google-cloud/pubsub');
    const {SubscriberClient} = require('@google-cloud/pubsub/build/src/v1');
    
    // Receives messages from a pull subscription.
    function receiveMessages() {
      const chat = new ChatServiceClient({
        keyFile: process.env.GOOGLE_APPLICATION_CREDENTIALS,
        scopes: ['https://www.googleapis.com/auth/chat.bot'],
      });
    
      const subscriptionPath = new SubscriberClient()
        .subscriptionPath(process.env.PROJECT_ID, process.env.SUBSCRIPTION_ID)
      const subscription = new PubSub()
        .subscription(subscriptionPath);
    
      // Handle incoming message, then ack/nack the received message
      const messageHandler = message => {
        console.log(`Id : ${message.id}`);
        const event = JSON.parse(message.data);
        console.log(`Data : ${JSON.stringify(event)}`);
    
        // Post the response to Google Chat.
        const request = formatRequest(event);
        if (request != null) {
          chat.createMessage(request);
        }
    
        // Ack the message.
        message.ack();
      }
    
      subscription.on('message', messageHandler);
      console.log(`Listening for messages on ${subscriptionPath}`);
    
      // Keep main thread from exiting while waiting for messages
      setTimeout(() => {
        subscription.removeListener('message', messageHandler);
        console.log(`Stopped listening for messages.`);
      }, 60 * 1000);
    }
    
    // Send message to Google Chat based on the type of event
    function formatRequest(event) {
      const spaceName = event.space.name;
      const eventType = event.type;
    
      // If the app was removed, we don't respond.
      if (event.type == 'REMOVED_FROM_SPACE') {
        console.log(`App removed rom space ${spaceName}`);
        return null;
      } else if (eventType == 'ADDED_TO_SPACE' && !eventType.message) {
        // 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.
        return {
          parent: spaceName,
          message: { text: 'Thank you for adding me!' }
        };
      } else if (eventType == 'ADDED_TO_SPACE' || eventType == 'MESSAGE') {
        // In case of message, post the response in the same thread.
        return {
          parent: spaceName,
          messageReplyOption: MessageReplyOption.REPLY_MESSAGE_FALLBACK_TO_NEW_THREAD,
          message: {
            text: 'You said: `' + event.message.text + '`',
            thread: { name: event.message.thread.name }
          }
        };
      }
    }
    
    if (!process.env.PROJECT_ID) {
      console.log('Missing PROJECT_ID env var.');
      process.exit(1);
    }
    if (!process.env.SUBSCRIPTION_ID) {
      console.log('Missing SUBSCRIPTION_ID env var.');
      process.exit(1);
    }
    if (!process.env.GOOGLE_APPLICATION_CREDENTIALS) {
      console.log('Missing GOOGLE_APPLICATION_CREDENTIALS env var.');
      process.exit(1);
    }
    
    receiveMessages();
    

將應用程式發布到 Chat

  1. 前往 Google Cloud 控制台中的「選單」 &gt; API 與Services (服務) &gt; 已啟用的 API 和Services (服務) &gt; Google Chat API &gt;「設定」

    前往「設定」

  2. 設定 Pub/Sub 的 Chat 應用程式:

    1. 在「App name」(應用程式名稱) 中輸入 Quickstart App
    2. 在「顯示圖片」中輸入 https://developers.google.com/chat/images/quickstart-app-avatar.png
    3. 在「Description」(說明) 中輸入 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

Python

python -m venv env
source env/bin/activate
pip install -r requirements.txt -U
python app.py

Node.js

npm install
npm start

執行程式碼時,應用程式會開始監聽發布的訊息 複製到 Pub/Sub 主題

測試 Chat 應用程式

如要測試 Chat 應用程式,請開啟含有下列對話的即時訊息聊天室: 傳送訊息:

  1. 使用你使用的 Google Workspace 帳戶開啟 Google Chat (在您將自己新增為信任的測試人員時提供)。

    前往 Google Chat

  2. 按一下「新的即時通訊」圖示
  3. 在 [新增 1 或多位使用者] 欄位中,輸入你的使用者名稱 Chat 應用程式。
  4. 從搜尋結果中選取 Chat 應用程式。直接 訊息隨即開啟。

  5. 開啟與應用程式互傳的新即時訊息,輸入 Hello,然後按下 enter

如要新增信任的測試人員並進一步瞭解如何測試互動功能,請參閱 測試互動式功能, Google Chat 應用程式

疑難排解

Google Chat 應用程式或 card 會傳回錯誤, 即時通訊介面顯示「發生錯誤」的訊息。 或「無法處理你的要求」。有時使用 Chat UI 不會顯示任何錯誤訊息,但 Chat 應用程式或 資訊卡產生非預期的結果例如資訊卡訊息 顯示。

雖然 Chat UI 中可能不會顯示錯誤訊息, 提供描述性錯誤訊息和記錄資料,協助您修正錯誤 。如需觀看說明, 偵錯及修正錯誤,請參閱 疑難排解並修正 Google Chat 錯誤

清除所用資源

如要避免系統向您的 Google Cloud 帳戶收取 建議將資源刪除 Cloud 專案

  1. 在 Google Cloud 控制台中,前往「管理資源」頁面。按一下 選單 &gt; IAM 與管理員 &gt;「管理資源」

    前往 Resource Manager

  2. 在專案清單中選取要刪除的專案,然後按一下 刪除
  3. 在對話方塊中輸入專案 ID,然後按一下「Shut down」(關閉) 即可刪除 專案。