Pub/Sub を使用してファイアウォールの背後で Google Chat アプリを構築する

このページでは、Pub/Sub を使用して Chat アプリを作成する方法について説明します。Chat アプリ向けのこのタイプのアーキテクチャは、組織にファイアウォールがあり、Chat から Chat アプリにメッセージが送信されない場合や、Chat アプリが Google Workspace Events API を使用している場合に役立ちます。ただし、Chat アプリは非同期メッセージのみを送受信できるため、このアーキテクチャには次の制限があります。

  • メッセージでダイアログは使用できません。代わりにカード メッセージを使用してください。
  • 同期レスポンスで個々のカードを更新することはできません。代わりに、patch メソッドを呼び出してメッセージ全体を更新します。

次の図は、Pub/Sub を使用してビルドされた Chat アプリのアーキテクチャを示しています。

Pub/Sub を使用して実装された Chat アプリのアーキテクチャ。

上の図では、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 プロジェクトで API を有効にする必要があります。1 つの Google Cloud プロジェクトで 1 つ以上の API を有効にできます。
  • Google Cloud コンソールで、Google Chat API と Pub/Sub API を有効にします。

    API を有効にする

Pub/Sub を設定する

  1. Chat API がメッセージを送信できる Pub/Sub トピックを作成します。Chat アプリごとに 1 つのトピックを使用することをおすすめします。

  2. 次のサービス アカウントに Pub/Sub パブリッシャーのロールを割り当てて、トピックにパブリッシュする権限を Chat に付与します。

    chat-api-push@system.gserviceaccount.com
    
  3. Chat アプリが Pub/Sub と Chat で承認するためのサービス アカウントを作成し、秘密鍵ファイルを作業ディレクトリに保存します。

  4. トピックへの pull サブスクリプションを作成します。

  5. 以前に作成したサービス アカウントのサブスクリプションに Pub/Sub サブスクライバー ロールを割り当てます

スクリプトを作成する

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. [アバターの URL] に「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. アプリでダイレクト メッセージを開くには、クイックスタート アプリを見つけて、[追加] > [チャット] をクリックします。
  5. ダイレクト メッセージに「Hello」と入力して、enter キーを押します。Chat アプリにメッセージがエコーバックされます。

Trusted Tester を追加し、インタラクティブ機能のテストの詳細については、Google Chat アプリのインタラクティブ機能をテストするをご覧ください。

トラブルシューティング

Google Chat アプリまたはカードからエラーが返されると、Chat インターフェースに「エラーが発生しました」または「リクエストを処理できません」というメッセージが表示されます。Chat UI にエラー メッセージが表示されないにもかかわらず、Chat アプリまたはカードから予期しない結果(カード メッセージが表示されないなど)が発生することがあります。

Chat の UI にエラー メッセージが表示されない場合もありますが、Chat アプリのエラーロギングがオンになっている場合は、エラー メッセージとログデータを使用してエラーを修正できます。エラーの表示、デバッグ、修正については、Google Chat のエラーのトラブルシューティングと修正をご覧ください。

クリーンアップ

このチュートリアルで使用したリソースについて、Google Cloud アカウントに課金されないようにするには、Cloud プロジェクトを削除することをおすすめします。

  1. Google Cloud コンソールで、[リソースの管理] ページに移動します。メニュー > [IAM と管理] > [リソースの管理] をクリックします。

    Resource Manager に移動

  2. プロジェクト リストで、削除するプロジェクトを選択し、[削除] をクリックします。
  3. ダイアログでプロジェクト ID を入力し、[シャットダウン] をクリックしてプロジェクトを削除します。