Using Cloud Pub/Sub as an endpoint for your bot

When creating a bot for your organization, your organization may be using a firewall, which can prevent Hangouts Chat from sending messages to your bot. To help with this, Hangouts Chat features an integration with Cloud Pub/Sub. This integration lets your bot establish a connection to Cloud Pub/Sub and receive messages.

Setting up Cloud Pub/Sub

This section describes how to set up a Cloud Pub/Sub topic and subscribe your bot to it.

Create a Pub/Sub-enabled Google Cloud project

To use Cloud Pub/Sub with your bot, you need to create a Google Cloud Platform project that has the Pub/Sub API enabled.

  1. Create a new project in the Cloud Platform Console.
  2. In the left pane of the console, select Pub/Sub and then select Enable API.

Create a Pub/Sub topic

Next, create a topic that the Chat API should send messages to. See the Pub/Sub Console quickstart to see how to create a topic.

Enable the Hangouts Chat API

Make sure to enable the Hangouts Chat API and set it to the topic created in the previous step.

  1. In the left pane of the console, select APIs & Services and enable the Hangouts Chat API.
  2. When configuring the API, make sure the Connection Settings are set to Cloud Pub/Sub and provide the same topic from the previous step.
  3. Fill out the other fields as detailed in the sample bot Guide.

Grant publish rights on your topic

In order for Hangouts Chat to publish messages to your topic, it must have publishing rights to the topic. To grant Hangouts Chat these permissions, assign the Pub/Sub Publisher role to the following service account:

serviceAccount:chat-api-push@system.gserviceaccount.com

This will authorize Hangouts Chat to publish on your topic.

Create a service account

In order for your bot code to authorize with Cloud Pub/Sub and Hangouts Chat, it needs to use a service account. Follow the Getting Started with Authentication guide to set up a new service account; then download and save the private key JSON file.

Create a subscription

Follow the Cloud Pub/Sub Subscriber Guide to set up a subscription to the topic that you created. Configure the subscription type to be a webhook pull. Add permissions on the subscription for the service account you created in the last step and give it 'Pub/Sub Subscriber' Role.

Publish the Cloud Pub/Sub bot

See the Publishing bots guide for details of how to publish your bot. For Cloud Pub/Sub bots, select the option for Cloud Pub/Sub and enter the fully qualified name of the topic created. The topic name must be in the following format:

projects/PROJECT_ID/topics/TOPIC_ID

For example, projects/pubsub-demo-2/topics/test-topic

Once you've published the bot in this way, users will be able to use it and the bot will receive messages on the Pub/Sub topic that you configured.

The example in the next section shows how to create and run a simple bot that implements these objects.

Example bot implementation

The sample code below shows a simple bot that uses Cloud Pub/Sub to receive incoming messages. To try out this bot, you need to:

  • Edit the project Id and subscription ID values in the Main class
  • Provide service account credentials as described in the Getting Started with Authentication guide.

    export GOOGLE_APPLICATIONCREDENTIALS=<path_to_service_account_file>

To run this code, you'll need the following dependencies in your classpath:

package com.google.chat;

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 = "my-project-id";

  // Cloud Pub/Sub Subscription ID
  public static final String SUBSCRIPTION_ID = "my-subscription-id";

  public static void main(String[] args) throws Exception {
    ProjectSubscriptionName subscriptionName =
        ProjectSubscriptionName.of(PROJECT_ID, SUBSCRIPTION_ID);

    // Instantiate bot, which implements an asynchronous message receiver.
    EchoBot echoBot = new EchoBot();

    // Create a subscriber for "my-subscription-id" bound to the message receiver
    final Subscriber subscriber =
        Subscriber.newBuilder(subscriptionName, echoBot).build();
    System.out.println("Starting subscriber...");
    subscriber.startAsync();

    // Wait for termination
    subscriber.awaitTerminated();
  }
}

/**
 * A demo bot which implements {@link MessageReceiver} to receive messages. It simply echoes the
 * incoming messages.
 */
class EchoBot implements MessageReceiver {

  // Path to the private key JSON file of the service account to be used for posting response
  // messages to Hangouts Chat.
  // In this demo, we are using the same service account for authorizing with Cloud Pub/Sub to
  // receive messages and authorizing with Hangouts 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 Hangouts Chat here.
  private static final String SERVICE_ACCOUNT_KEY_PATH =
      System.getenv(Main.CREDENTIALS_PATH_ENV_PROPERTY);

  // Developer code for Hangouts Chat api scope.
  private static final String HANGOUTS_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;

  EchoBot() throws Exception {
    credential =
        GoogleCredential.fromStream(new FileInputStream(SERVICE_ACCOUNT_KEY_PATH))
            .createScoped(Collections.singleton(HANGOUTS_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);
        // A bot can also be added to a room by @mentioning it in a message. In that case, we fall
        // through to the MESSAGE case and let the bot respond. If the bot 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 Hangouts 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();
  }
}