Compila una app de Google Chat detrás de un firewall con Pub/Sub

En esta página, se explica cómo crear una app de chat con Pub/Sub. Este tipo de arquitectura para una app de Chat es útil si tu organización tiene un firewall, que puede impedir que Chat envíe mensajes a tu app de Chat o si la app de Chat usa la API de Google Workspace Events. Sin embargo, esta arquitectura tiene las siguientes limitaciones debido a que estas apps de chat solo pueden enviar y recibir mensajes asíncronos:

  • No se pueden usar diálogos en los mensajes. En su lugar, usa un mensaje de tarjeta.
  • No se pueden actualizar tarjetas individuales con una respuesta síncrona. En su lugar, llama al método patch para actualizar todo el mensaje.

En el siguiente diagrama, se muestra la arquitectura de una app de chat compilada con Pub/Sub:

Arquitectura de una app de Chat implementada con Pub/Sub.

En el diagrama anterior, un usuario que interactúa con una app de chat de Pub/Sub tiene el siguiente flujo de información:

  1. Un usuario envía un mensaje en Chat a una app de Chat, ya sea en un mensaje directo o en un espacio de Chat, o bien se produce un evento en un espacio de Chat para el que la app de Chat tiene una suscripción activa.

  2. Chat envía el mensaje a un tema de Pub/Sub.

  3. Un servidor de aplicaciones, que es un sistema en las instalaciones o en la nube que contiene la lógica de la app de Chat, se suscribe al tema de Pub/Sub para recibir el mensaje a través del firewall.

  4. De manera opcional, la app de Chat puede llamar a la API de Chat para publicar mensajes de manera asíncrona o realizar otras operaciones.

Requisitos previos

Java

Python

Node.js

Configura el entorno

Antes de usar las APIs de Google, debes activarlas en un proyecto de Google Cloud. Puedes activar una o más APIs en un solo proyecto de Google Cloud.
  • En la consola de Google Cloud, habilita la API de Google Chat y la API de Pub/Sub.

    Habilitar las API

Configura Pub/Sub

  1. Crea un tema de Pub/Sub al que la API de Chat pueda enviar mensajes. Te recomendamos que uses un solo tema por app de Chat.

  2. Otorga permiso a Chat para publicar en el tema asignando el rol de Publicador de Pub/Sub a la siguiente cuenta de servicio:

    chat-api-push@system.gserviceaccount.com
    
  3. Crea una cuenta de servicio para que la app de Chat la autorice con Pub/Sub y Chat, y guarda el archivo de clave privada en tu directorio de trabajo.

  4. Crea una suscripción de extracción al tema.

  5. Asigna el rol de suscriptor de Pub/Sub en la suscripción de la cuenta de servicio que creaste antes.

Escribe el guion

Java

  1. En una CLI, proporciona las credenciales de la cuenta de servicio:

    export GOOGLE_APPLICATION_CREDENTIALS=SERVICE_ACCOUNT_FILE_PATH
    
  2. En una CLI, proporciona el ID del proyecto de Google Cloud:

    export PROJECT_ID=PROJECT_ID
    
  3. En una CLI, proporciona el ID de suscripción de la suscripción a Pub/Sub que creaste anteriormente:

    export SUBSCRIPTION_ID=SUBSCRIPTION_ID
    
  4. En tu directorio de trabajo, crea un archivo llamado pom.xml.

  5. En el archivo pom.xml, pega el siguiente código:

    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. En tu directorio de trabajo, crea la estructura de directorio src/main/java.

  7. En el directorio src/main/java, crea un archivo llamado Main.java.

  8. En Main.java, pega el siguiente código:

    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. En una CLI, proporciona las credenciales de la cuenta de servicio:

    export GOOGLE_APPLICATION_CREDENTIALS=SERVICE_ACCOUNT_FILE_PATH
    
  2. En una CLI, proporciona el ID del proyecto de Google Cloud:

    export PROJECT_ID=PROJECT_ID
    
  3. En una CLI, proporciona el ID de suscripción de la suscripción a Pub/Sub que creaste anteriormente:

    export SUBSCRIPTION_ID=SUBSCRIPTION_ID
    
  4. En tu directorio de trabajo, crea un archivo llamado requirements.txt.

  5. En el archivo requirements.txt, pega el siguiente código:

    python/pub-sub-app/requirements.txt
    google-cloud-pubsub>=2.23.0
    google-apps-chat==0.1.9
  6. En tu directorio de trabajo, crea un archivo llamado app.py.

  7. En app.py, pega el siguiente código:

    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. En una CLI, proporciona las credenciales de la cuenta de servicio:

    export GOOGLE_APPLICATION_CREDENTIALS=SERVICE_ACCOUNT_FILE_PATH
    
  2. En una CLI, proporciona el ID del proyecto de Google Cloud:

    export PROJECT_ID=PROJECT_ID
    
  3. En una CLI, proporciona el ID de suscripción de la suscripción a Pub/Sub que creaste anteriormente:

    export SUBSCRIPTION_ID=SUBSCRIPTION_ID
    
  4. En tu directorio de trabajo, crea un archivo llamado package.json.

  5. En el archivo package.json, pega el siguiente código:

    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. En tu directorio de trabajo, crea un archivo llamado index.js.

  7. En index.js, pega el siguiente código:

    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();

Publica la app en Chat

  1. En la consola de Google Cloud, ve a Menú > APIs y servicios > APIs y servicios habilitados > API de Google Chat > Configuración.

    Ir a Configuración

  2. Configura la app de Chat para Pub/Sub:

    1. En Nombre de la app, ingresa Quickstart App.
    2. En URL del avatar, escribe https://developers.google.com/chat/images/quickstart-app-avatar.png.
    3. En Descripción, ingresa Quickstart app.
    4. En Funcionalidad, selecciona Recibir mensajes 1:1 y Unirse a espacios y conversaciones grupales.
    5. En Configuración de la conexión, selecciona Cloud Pub/Sub y pega el nombre del tema de Pub/Sub que creaste antes.
    6. En Visibilidad, selecciona Hacer que esta app de Google Chat esté disponible para personas y grupos específicos en tu dominio y escribe tu dirección de correo electrónico.
    7. En Registros, selecciona Registrar errores en Logging.
  3. Haz clic en Guardar.

La app ya está lista para recibir y responder mensajes en Chat.

Ejecuta la secuencia de comandos:

En una CLI, cambia al directorio de trabajo y ejecuta la secuencia de comandos:

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

Cuando ejecutas el código, la aplicación comienza a escuchar los mensajes publicados en el tema de Pub/Sub.

Prueba tu app de Chat

Para probar tu app de Chat, abre un espacio de mensajes directos con la app de Chat y envía un mensaje:

  1. Abre Google Chat con la cuenta de Google Workspace que proporcionaste cuando te agregaste como verificador de confianza.

    Ve a Google Chat

  2. Haz clic en Nuevo chat.
  3. En el campo Agregar 1 o más personas, escribe el nombre de tu app de Chat.
  4. Selecciona tu app de Chat en los resultados. Se abrirá un mensaje directo.

  5. En el nuevo mensaje directo con la app, escribe Hello y presiona enter.

Para agregar verificadores de confianza y obtener más información sobre cómo probar funciones interactivas, consulta Cómo probar funciones interactivas para apps de Google Chat.

Solucionar problemas

Cuando una app de Google Chat o una tarjeta muestran un error, la interfaz de Chat muestra un mensaje que dice "Se produjo un error". o "No se puede procesar tu solicitud". A veces, la IU de Chat no muestra ningún mensaje de error, pero la app o la tarjeta de Chat producen un resultado inesperado. Por ejemplo, es posible que no aparezca un mensaje de la tarjeta.

Aunque es posible que no se muestre un mensaje de error en la IU de Chat, los mensajes de error descriptivos y los datos de registro están disponibles para ayudarte a corregir errores cuando se activa el registro de errores de las apps de Chat. Si necesitas ayuda para ver, depurar y corregir errores, consulta Cómo solucionar problemas y corregir errores de Google Chat.

Limpia

Para evitar que se apliquen cargos a tu cuenta de Google Cloud por los recursos que usaste en este instructivo, te recomendamos que borres el proyecto de Cloud.

  1. En la consola de Google Cloud, ve a la página Administrar recursos. Haz clic en Menú > IAM y administración > Administrar recursos.

    Ve al Administrador de recursos.

  2. En la lista de proyectos, selecciona el proyecto que deseas borrar y haz clic en Borrar .
  3. En el cuadro de diálogo, escribe el ID del proyecto y, luego, haz clic en Cerrar para borrarlo.