Compila una app de encuestas interactiva para Google Chat con Node.js

1. Introducción

Con las apps de Google Chat, tus servicios y recursos estarán disponibles en Google Chat, lo que les permitirá a los usuarios obtener información y realizar acciones rápidas sin salir de la conversación.

En este codelab, aprenderás a compilar e implementar una app de sondeo con Node.js y Cloud Functions.

Qué aprenderás

  • Usar Cloud Shell
  • Implementa en Cloud Functions
  • Obtén entradas del usuario con comandos de barra y diálogos
  • Creación de tarjetas interactivas

2. Configuración y requisitos

Cree un proyecto de Google Cloud y, luego, habilite las API y los servicios que usará la aplicación de Chat

Requisitos previos

Para desarrollar una app de Google Chat, se requiere una cuenta de Google Workspace con acceso a Google Chat. Si aún no tienes una cuenta de Google Workspace, crea una y accede antes de continuar con este codelab.

Configuración del entorno a su propio ritmo

  1. Abre Google Cloud Console y crea un proyecto.

    El menú Seleccionar un proyectoEl nuevo botón ProyectoEl ID del proyecto

    Recuerde el ID del proyecto, un nombre único en todos los proyectos de Google Cloud (el nombre anterior ya está en uso y no funcionará. Se mencionará más adelante en este codelab como PROJECT_ID.
  1. A continuación, para usar los recursos de Google Cloud, habilite la facturación en Cloud Console.

Ejecutar este codelab no debería costar mucho, tal vez nada. Asegúrate de seguir las instrucciones de la sección "Cómo limpiar" que se encuentra al final del codelab, en la que se indica cómo cerrar recursos para no incurrir en facturación más allá de este instructivo. Los usuarios nuevos de Google Cloud son aptos para participar en el programa Prueba gratuita de $300.

Google Cloud Shell

Si bien Google Cloud se puede operar de forma remota desde tu laptop, en este codelab usaremos Google Cloud Shell, un entorno de línea de comandos que se ejecuta en Google Cloud.

Active Cloud Shell

  1. En Cloud Console, haz clic en Activar Cloud Shell El ícono de Cloud Shell.

    El ícono de Cloud Shell en la barra de menú

    La primera vez que abras Cloud Shell, verás un mensaje de bienvenida descriptivo. Si ve el mensaje de bienvenida, haga clic en Continuar. El mensaje de bienvenida no volverá a aparecer. Este es el mensaje de bienvenida:

    Mensaje de bienvenida de Cloud Shell

    El aprovisionamiento y la conexión a Cloud Shell debería tardar solo unos minutos. Después de conectarse, verá la terminal de Cloud Shell:

    La terminal de Cloud Shell

    Esta máquina virtual está cargada con todas las herramientas para desarrolladores que necesita. Ofrece un directorio principal persistente de 5 GB y se ejecuta en Google Cloud, lo que permite mejorar considerablemente el rendimiento de la red y la autenticación. Puedes realizar todo el trabajo en este codelab con un navegador o tu Chromebook.Una vez conectado a Cloud Shell, deberías ver que ya estás autenticado y que el proyecto ya tiene asignado el ID.
  2. En Cloud Shell, ejecute el siguiente comando para confirmar que está autenticado:
    gcloud auth list
    
    Si se te solicita que autorices Cloud Shell a realizar una llamada a la API de GCP, haz clic en Autorizar.

    Resultado del comando
    Credentialed Accounts
    ACTIVE  ACCOUNT
    *       <my_account>@<my_domain.com>
    
    Si tu cuenta no está seleccionada de forma predeterminada, ejecuta lo siguiente:
    $ gcloud config set account <ACCOUNT>
    
  1. Confirma que seleccionaste el proyecto correcto. En Cloud Shell, ejecute el siguiente comando:
    gcloud config list project
    
    Resultado del comando
    [core]
    project = <PROJECT_ID>
    
    Si no se muestra el proyecto correcto, puedes configurarlo con el siguiente comando:
    gcloud config set project <PROJECT_ID>
    
    Resultado del comando
    Updated property [core/project].
    

A medida que completes este codelab, usarás operaciones de líneas de comandos y archivos. Para editar archivos, puede usar el editor de código integrado de Cloud ShellEditor de Cloud Shell , haciendo clic enAbrir editor en el lado derecho de la barra de herramientas de Cloud Shell. Los editores populares como Vim y Emacs también están disponibles en Cloud Shell.

3. Habilita las API de Cloud Functions, Cloud Build y Google Chat

En Cloud Shell, habilite las siguientes API y servicios:

gcloud services enable \
  cloudfunctions \
  cloudbuild.googleapis.com \
  chat.googleapis.com

Esta operación puede tardar unos minutos en completarse.

Cuando se complete, aparecerá un mensaje de éxito similar al siguiente:

Operation "operations/acf.cc11852d-40af-47ad-9d59-477a12847c9e" finished successfully.

4. Crea la app inicial de Chat

Inicializa el proyecto

Para comenzar, crearás e implementarás una app simple de "Hello World". Las apps de chat son servicios web que responden a solicitudes HTTPS y responden con una carga útil JSON. Para esta app, usarás Node.js y Cloud Functions.

En Cloud Shell, crea un directorio nuevo llamado poll-app y navega hasta él:

mkdir ~/poll-app
cd ~/poll-app

En este directorio, encontrarás todo el trabajo restante del codelab y los archivos que crearás.

Inicializa el proyecto de Node.js:

npm init

NPM hace varias preguntas sobre la configuración del proyecto, como el nombre y la versión. En cada pregunta, presiona ENTER para aceptar los valores predeterminados. El punto de entrada predeterminado es un archivo llamado index.js, que crearemos a continuación.

Crea el backend de la app de Chat

Es hora de comenzar a crear la app. Crea un archivo llamado index.js con el siguiente contenido:

/**
 * App entry point.
 */
exports.app = async (req, res) => {
  if (!(req.method === 'POST' && req.body)) {
      res.status(400).send('')
  }
  const event = req.body;
  let reply = {};
  if (event.type === 'MESSAGE') {
    reply = {
        text: `Hello ${event.user.displayName}`
    };
  }
  res.json(reply)
}

La app no hará mucho, pero no te preocupes. Agregarás más funcionalidad más adelante.

Implementa la app

Para implementar la app “Hello World”, implementarás la Cloud Function, configurarás la app de Chat en Google Cloud Console y enviarás un mensaje de prueba a la app para verificar la implementación.

Implemente la Cloud Function

Para implementar la Cloud Function de la app “Hello World”, ingresa el siguiente comando:

gcloud functions deploy app --trigger-http --security-level=secure-always --allow-unauthenticated --runtime nodejs14

Cuando termine, el resultado debería ser similar a este:

availableMemoryMb: 256
buildId: 993b2ca9-2719-40af-86e4-42c8e4563a4b
buildName: projects/595241540133/locations/us-central1/builds/993b2ca9-2719-40af-86e4-42c8e4563a4b
entryPoint: app
httpsTrigger:
  securityLevel: SECURE_ALWAYS
  url: https://us-central1-poll-app-codelab.cloudfunctions.net/app
ingressSettings: ALLOW_ALL
labels:
  deployment-tool: cli-gcloud
name: projects/poll-app-codelab/locations/us-central1/functions/app
runtime: nodejs14
serviceAccountEmail: poll-app-codelab@appspot.gserviceaccount.com
sourceUploadUrl: https://storage.googleapis.com/gcf-upload-us-central1-66a01777-67f0-46d7-a941-079c24414822/94057943-2b7c-4b4c-9a21-bb3acffc84c6.zip
status: ACTIVE
timeout: 60s
updateTime: '2021-09-17T19:30:33.694Z'
versionId: '1'

Observa la URL de la función implementada en la propiedad httpsTrigger.url. La usarás en el siguiente paso.

Configura la app

Para configurar la app, ve aConfiguración de chat en Cloud Console (API y servicios Más dePanel Más deAPI de Hangouts Chat Más deConfiguración ).

  1. En App name, ingresa "PollCodelab".
  2. En Avatar URL, ingresa https://raw.githubusercontent.com/google/material-design-icons/master/png/social/poll/materialicons/24dp/2x/baseline_poll_black_24dp.png.
  3. En Description, ingresa "Poll app for codelab".
  4. En Funcionalidad, selecciona Se puede enviar un mensaje directamente a la app y La app funciona en espacios con varios usuarios.
  5. En Configuración de conexión, selecciona URL de la app y pega la URL de la función de Cloud Functions (la propiedad httpsTrigger.url de la última sección).
  6. En Permisos, selecciona Personas y grupos específicos en tu dominio y, luego, ingresa tu dirección de correo electrónico.
  7. Haga clic en Guardar.

La app ya está lista para enviar mensajes.

Prueba la app

Antes de continuar, comprueba que la app funcione. Para ello, agrégala a un espacio en Google Chat.

  1. Ir a Google Chat
  2. Junto a Chat, haz clic en + > Buscar apps.
  3. Ingresa "PollCodelab" en la búsqueda.
  4. Haz clic en Chat.
  5. Para enviar un mensaje a la app, escribe "Hello" y presiona Intro.

La app debería responder con un breve mensaje de saludo.

Ahora que ya tienes un esqueleto básico, es hora de convertirlo en algo más útil.

5. Compila las funciones de encuestas

Descripción general del funcionamiento de la app

La app consta de dos partes principales:

  1. Un comando de barra que muestra un diálogo para configurar la encuesta.
  2. Una tarjeta interactiva para votar y ver los resultados.

La app también necesita cierto estado para almacenar la configuración de la encuesta y los resultados. Esto se puede hacer con Firestore o con cualquier base de datos, o se puede almacenar el estado en los mensajes de la aplicación. Como esta app está destinada a encuestas informales rápidas de un equipo, el almacenamiento del estado en los mensajes de la app funciona de manera excelente para este caso de uso.

El modelo de datos para la app (expresado en Typescript) es el siguiente:

interface Poll {
  /* Question/topic of poll */
  topic: string;
  /** User that submitted the poll */
  author: {
    /** Unique resource name of user */
    name: string;
    /** Display name */
    displayName: string;
  };
  /** Available choices to present to users */
  choices: string[];
  /** Map of user ids to the index of their selected choice */
  votes: { [key: string]: number };
}

Además del tema o la pregunta y una lista de opciones, el estado incluye el ID y el nombre del autor, así como los votos registrados. Para evitar que los usuarios voten varias veces, los votos se almacenan como un mapa de los ID de los usuarios con el índice que hayan elegido.

Existen, por supuesto, muchos enfoques diferentes, pero esto proporciona un buen punto de partida para ejecutar encuestas rápidas en un espacio.

Implementa el comando de configuración de sondeo

Para permitir que los usuarios inicien y configuren las encuestas, configura un comando de barra que abra un diálogo. Este proceso consta de varios pasos:

  1. Registra el comando de barra que inicia una encuesta.
  2. Crea el diálogo para configurar una encuesta.
  3. Permite que la app reconozca y controle el comando de barra.
  4. Crea tarjetas interactivas que faciliten la votación en la encuesta.
  5. Implementa el código que permite que la app ejecute encuestas.
  6. Vuelva a implementar la Cloud Function.

Registra el comando de barra

Para registrar un comando de barra, vuelva a la página Configuración de chat en Console (API y servicios > Panel > API de Hangouts Chat) > Configuración).

  1. En Comandos de barra, haz clic en Agregar comando de barra.
  2. En Nombre, ingrese "/poll".
  3. En ID de comando, ingresa "1".
  4. En Descripción, ingresa "Iniciar una encuesta".
  5. Selecciona Abre un diálogo.
  6. Haga clic en Listo.
  7. Haz clic en Guardar.

Ahora, la app reconoce el comando /poll y abre un diálogo. A continuación, configuremos el diálogo.

Crea el formulario de configuración como un diálogo

El comando de barra abre un diálogo para configurar el tema de la encuesta y las posibles opciones. Crea un archivo nuevo llamado config-form.js con el siguiente contenido:

/** Upper bounds on number of choices to present. */
const MAX_NUM_OF_OPTIONS = 5;

/**
 * Build widget with instructions on how to use form.
 *
 * @returns {object} card widget
 */
function helpText() {
  return {
    textParagraph: {
      text: 'Enter the poll topic and up to 5 choices in the poll. Blank options will be omitted.',
    },
  };
}

/**
 * Build the text input for a choice.
 *
 * @param {number} index - Index to identify the choice
 * @param {string|undefined} value - Initial value to render (optional)
 * @returns {object} card widget
 */
function optionInput(index, value) {
  return {
    textInput: {
      label: `Option ${index + 1}`,
      type: 'SINGLE_LINE',
      name: `option${index}`,
      value: value || '',
    },
  };
}

/**
 * Build the text input for the poll topic.
 *
 * @param {string|undefined} topic - Initial value to render (optional)
 * @returns {object} card widget
 */
function topicInput(topic) {
  return {
    textInput: {
      label: 'Topic',
      type: 'MULTIPLE_LINE',
      name: 'topic',
      value: topic || '',
    },
  };
}

/**
 * Build the buttons/actions for the form.
 *
 * @returns {object} card widget
 */
function buttons() {
  return {
    buttonList: {
      buttons: [
        {
          text: 'Submit',
          onClick: {
            action: {
              function: 'start_poll',
            },
          },
        },
      ],
    },
  };
}

/**
 * Build the configuration form.
 *
 * @param {object} options - Initial state to render with form
 * @param {string|undefined} options.topic - Topic of poll (optional)
 * @param {string[]|undefined} options.choices - Text of choices to display to users (optional)
 * @returns {object} card
 */
function buildConfigurationForm(options) {
  const widgets = [];
  widgets.push(helpText());
  widgets.push(topicInput(options.topic));
  for (let i = 0; i < MAX_NUM_OF_OPTIONS; ++i) {
    const choice = options?.choices?.[i];
    widgets.push(optionInput(i, choice));
  }
  widgets.push(buttons());

  // Assemble the card
  return {
    sections: [
      {
        widgets,
      },
    ],
  };
}

exports.MAX_NUM_OF_OPTIONS = MAX_NUM_OF_OPTIONS;
exports.buildConfigurationForm = buildConfigurationForm;

Este código genera el formulario de diálogo que le permite al usuario configurar la encuesta. También exporta una constante para la cantidad máxima de opciones que puede tener una pregunta. Se recomienda aislar la compilación del lenguaje de marcado de la IU en funciones sin estado con cualquier estado pasado como parámetros. Facilita la reutilización y, luego, esta tarjeta se renderizará en diferentes contextos.

Esta implementación también descompone la tarjeta en unidades o componentes más pequeños. Si bien no es obligatoria, la técnica es una práctica recomendada porque tiende a ser más legible y fácil de mantener cuando se compilan interfaces complejas.

Para ver una muestra de la versión de JSON completa que compila, consulta la herramienta del Creador de tarjetas.

Controla el comando de barra

Los comandos de barra aparecen como eventos MESSAGE cuando se envían a la app. Actualiza index.js a fin de verificar la presencia de un comando de barra mediante un evento MESSAGE y para responder con un diálogo. Reemplaza index.js por lo siguiente:

const { buildConfigurationForm, MAX_NUM_OF_OPTIONS } = require('./config-form');

/**
 * App entry point.
 */
exports.app = async (req, res) => {
  if (!(req.method === 'POST' && req.body)) {
      res.status(400).send('')
  }
  const event = req.body;
  let reply = {};
  // Dispatch slash and action events
  if (event.type === 'MESSAGE') {
    const message = event.message;
    if (message.slashCommand?.commandId === '1') {
      reply = showConfigurationForm(event);
    }
  } else if (event.type === 'CARD_CLICKED') {
    if (event.action?.actionMethodName === 'start_poll') {
      reply = await startPoll(event);
    }
  }
  res.json(reply);
}

/**
 * Handles the slash command to display the config form.
 *
 * @param {object} event - chat event
 * @returns {object} Response to send back to Chat
 */
function showConfigurationForm(event) {
  // Seed the topic with any text after the slash command
  const topic = event.message?.argumentText?.trim();
  const dialog = buildConfigurationForm({
    topic,
    choices: [],
  });
  return {
    actionResponse: {
      type: 'DIALOG',
      dialogAction: {
        dialog: {
          body: dialog,
        },
      },
    },
  };
}

/**
 * Handle the custom start_poll action.
 *
 * @param {object} event - chat event
 * @returns {object} Response to send back to Chat
 */
function startPoll(event) {
  // Not fully implemented yet -- just close the dialog
  return {
    actionResponse: {
      type: 'DIALOG',
      dialogAction: {
        actionStatus: {
          statusCode: 'OK',
          userFacingMessage: 'Poll started.',
        },
      },
    },
  }
}

Ahora, la app mostrará un diálogo con la invocación del comando /poll. Probar la interacción mediante la implementación de Cloud Functions desde Cloud Shell

gcloud functions deploy app --trigger-http --security-level=secure-always

Después de que se implemente la Cloud Function, envíe un mensaje a la app con el comando /poll para probar el comando de barra y el diálogo. El diálogo envía un evento CARD_CLICKED con la acción personalizada start_poll. El evento se controla en el punto de entrada actualizado, en el que llama al método startPoll. Por ahora, el método startPoll se utiliza en stub para cerrar el diálogo. En la próxima sección, implementarás la funcionalidad de votación y conectarás todas las partes.

Implementa la tarjeta de votación

Para implementar la parte de votación de la app, comienza por definir la tarjeta interactiva que proporciona una interfaz para votos.

Implementa la interfaz de votación

Crea un archivo llamado vote-card.js con el siguiente contenido:

/**
 * Creates a small progress bar to show percent of votes for an option. Since
 * width is limited, the percentage is scaled to 20 steps (5% increments).
 *
 * @param {number} voteCount - Number of votes for this option
 * @param {number} totalVotes - Total votes cast in the poll
 * @returns {string} Text snippet with bar and vote totals
 */
function progressBarText(voteCount, totalVotes) {
  if (voteCount === 0 || totalVotes === 0) {
    return '';
  }

  // For progress bar, calculate share of votes and scale it
  const percentage = (voteCount * 100) / totalVotes;
  const progress = Math.round((percentage / 100) * 20);
  return '▀'.repeat(progress);
}

/**
 * Builds a line in the card for a single choice, including
 * the current totals and voting action.
 *
 * @param {number} index - Index to identify the choice
 * @param {string|undefined} value - Text of the choice
 * @param {number} voteCount - Current number of votes cast for this item
 * @param {number} totalVotes - Total votes cast in poll
 * @param {string} state - Serialized state to send in events
 * @returns {object} card widget
 */
function choice(index, text, voteCount, totalVotes, state) {
  const progressBar = progressBarText(voteCount, totalVotes);
  return {
    keyValue: {
      bottomLabel: `${progressBar} ${voteCount}`,
      content: text,
      button: {
        textButton: {
          text: 'vote',
          onClick: {
            action: {
              actionMethodName: 'vote',
              parameters: [
                {
                  key: 'state',
                  value: state,
                },
                {
                  key: 'index',
                  value: index.toString(10),
                },
              ],
            },
          },
        },
      },
    },
  };
}

/**
 * Builds the card header including the question and author details.
 *
 * @param {string} topic - Topic of the poll
 * @param {string} author - Display name of user that created the poll
 * @returns {object} card widget
 */
function header(topic, author) {
  return {
    title: topic,
    subtitle: `Posted by ${author}`,
    imageUrl:
      'https://raw.githubusercontent.com/google/material-design-icons/master/png/social/poll/materialicons/24dp/2x/baseline_poll_black_24dp.png',
    imageStyle: 'AVATAR',
  };
}

/**
 * Builds the configuration form.
 *
 * @param {object} poll - Current state of poll
 * @param {object} poll.author - User that submitted the poll
 * @param {string} poll.topic - Topic of poll
 * @param {string[]} poll.choices - Text of choices to display to users
 * @param {object} poll.votes - Map of cast votes keyed by user ids
 * @returns {object} card
 */
function buildVoteCard(poll) {
  const widgets = [];
  const state = JSON.stringify(poll);
  const totalVotes = Object.keys(poll.votes).length;

  for (let i = 0; i < poll.choices.length; ++i) {
    // Count votes for this choice
    const votes = Object.values(poll.votes).reduce((sum, vote) => {
      if (vote === i) {
        return sum + 1;
      }
      return sum;
    }, 0);
    widgets.push(choice(i, poll.choices[i], votes, totalVotes, state));
  }

  return {
    header: header(poll.topic, poll.author.displayName),
    sections: [
      {
        widgets,
      },
    ],
  };
}

exports.buildVoteCard = buildVoteCard;

La implementación es similar al enfoque adoptado con el diálogo, aunque el lenguaje de marcado de las tarjetas interactivas es ligeramente diferente de los diálogos. Como antes, puedes ver una muestra del archivo JSON generado en la herramienta del Creador de tarjetas.

Implementa la acción de votación

La tarjeta de votación incluye un botón para cada opción. El índice de esa opción, junto con el estado serializado de la encuesta, se adjuntará al botón. La app recibe un CARD_CLICKED con la acción vote junto con cualquier dato adjunto al botón como parámetros.

Actualiza index.js con lo siguiente:

const { buildConfigurationForm, MAX_NUM_OF_OPTIONS } = require('./config-form');
const { buildVoteCard } = require('./vote-card');

/**
 * App entry point.
 */
exports.app = async (req, res) => {
  if (!(req.method === 'POST' && req.body)) {
      res.status(400).send('')
  }
  const event = req.body;
  let reply = {};
  // Dispatch slash and action events
  if (event.type === 'MESSAGE') {
    const message = event.message;
    if (message.slashCommand?.commandId === '1') {
      reply = showConfigurationForm(event);
    }
  } else if (event.type === 'CARD_CLICKED') {
    if (event.action?.actionMethodName === 'start_poll') {
      reply = await startPoll(event);
    } else if (event.action?.actionMethodName === 'vote') {
        reply = recordVote(event);
    }
  }
  res.json(reply);
}

/**
 * Handles the slash command to display the config form.
 *
 * @param {object} event - chat event
 * @returns {object} Response to send back to Chat
 */
function showConfigurationForm(event) {
  // Seed the topic with any text after the slash command
  const topic = event.message?.argumentText?.trim();
  const dialog = buildConfigurationForm({
    topic,
    choices: [],
  });
  return {
    actionResponse: {
      type: 'DIALOG',
      dialogAction: {
        dialog: {
          body: dialog,
        },
      },
    },
  };
}

/**
 * Handle the custom start_poll action.
 *
 * @param {object} event - chat event
 * @returns {object} Response to send back to Chat
 */
function startPoll(event) {
  // Not fully implemented yet -- just close the dialog
  return {
    actionResponse: {
      type: 'DIALOG',
      dialogAction: {
        actionStatus: {
          statusCode: 'OK',
          userFacingMessage: 'Poll started.',
        },
      },
    },
  }
}

/**
 * Handle the custom vote action. Updates the state to record
 * the user's vote then rerenders the card.
 *
 * @param {object} event - chat event
 * @returns {object} Response to send back to Chat
 */
function recordVote(event) {
  const parameters = event.common?.parameters;

  const choice = parseInt(parameters['index']);
  const userId = event.user.name;
  const state = JSON.parse(parameters['state']);

  // Add or update the user's selected option
  state.votes[userId] = choice;

  const card = buildVoteCard(state);
  return {
    thread: event.message.thread,
    actionResponse: {
      type: 'UPDATE_MESSAGE',
    },
    cards: [card],
  }
}

El método recordVote analiza el estado almacenado y lo actualiza con el voto del usuario y, luego, vuelve a procesar la tarjeta. Los resultados de la encuesta se serializan y almacenan con la tarjeta cada vez que se actualizan.

Conecta las piezas

La app ya casi está lista. Con el comando de barra implementado junto con la votación, lo único que falta es finalizar el método startPoll.

Pero hay un truco.

Cuando se envía la configuración de sondeo, la app necesita realizar dos acciones:

  1. Cierra el diálogo.
  2. Publica un mensaje nuevo en el espacio con la tarjeta de votación.

Lamentablemente, la respuesta directa a la solicitud HTTP solo puede hacer una y debe ser la primera. Para publicar la tarjeta de votación, la app debe usar la API de Chat para crear un mensaje nuevo de forma asíncrona.

Agrega la biblioteca cliente

Ejecuta el siguiente comando a fin de actualizar las dependencias de la app para incluir el cliente de la API de Google para Node.js.

npm install --save googleapis

Iniciar la encuesta

Actualiza index.js a la versión final a continuación:

const { buildConfigurationForm, MAX_NUM_OF_OPTIONS } = require('./config-form');
const { buildVoteCard } = require('./vote-card');
const {google} = require('googleapis');

/**
 * App entry point.
 */
exports.app = async (req, res) => {
  if (!(req.method === 'POST' && req.body)) {
      res.status(400).send('')
  }
  const event = req.body;
  let reply = {};
  // Dispatch slash and action events
  if (event.type === 'MESSAGE') {
    const message = event.message;
    if (message.slashCommand?.commandId === '1') {
      reply = showConfigurationForm(event);
    }
  } else if (event.type === 'CARD_CLICKED') {
    if (event.action?.actionMethodName === 'start_poll') {
      reply = await startPoll(event);
    } else if (event.action?.actionMethodName === 'vote') {
        reply = recordVote(event);
    }
  }
  res.json(reply);
}

/**
 * Handles the slash command to display the config form.
 *
 * @param {object} event - chat event
 * @returns {object} Response to send back to Chat
 */
function showConfigurationForm(event) {
  // Seed the topic with any text after the slash command
  const topic = event.message?.argumentText?.trim();
  const dialog = buildConfigurationForm({
    topic,
    choices: [],
  });
  return {
    actionResponse: {
      type: 'DIALOG',
      dialogAction: {
        dialog: {
          body: dialog,
        },
      },
    },
  };
}

/**
 * Handle the custom start_poll action.
 *
 * @param {object} event - chat event
 * @returns {object} Response to send back to Chat
 */
async function startPoll(event) {
  // Get the form values
  const formValues = event.common?.formInputs;
  const topic = formValues?.['topic']?.stringInputs.value[0]?.trim();
  const choices = [];
  for (let i = 0; i < MAX_NUM_OF_OPTIONS; ++i) {
    const choice = formValues?.[`option${i}`]?.stringInputs.value[0]?.trim();
    if (choice) {
      choices.push(choice);
    }
  }

  if (!topic || choices.length === 0) {
    // Incomplete form submitted, rerender
    const dialog = buildConfigurationForm({
      topic,
      choices,
    });
    return {
      actionResponse: {
        type: 'DIALOG',
        dialogAction: {
          dialog: {
            body: dialog,
          },
        },
      },
    };
  }

  // Valid configuration, build the voting card to display
  // in the space
  const pollCard = buildVoteCard({
    topic: topic,
    author: event.user,
    choices: choices,
    votes: {},
  });
  const message = {
    cards: [pollCard],
  };
  const request = {
    parent: event.space.name,
    requestBody: message,
  };
  // Use default credentials (service account)
  const credentials = new google.auth.GoogleAuth({
    scopes: ['https://www.googleapis.com/auth/chat.bot'],
  });
  const chatApi = google.chat({
    version: 'v1',
    auth: credentials,
  });
  await chatApi.spaces.messages.create(request);

  // Close dialog
  return {
    actionResponse: {
      type: 'DIALOG',
      dialogAction: {
        actionStatus: {
          statusCode: 'OK',
          userFacingMessage: 'Poll started.',
        },
      },
    },
  };
}

/**
 * Handle the custom vote action. Updates the state to record
 * the user's vote then rerenders the card.
 *
 * @param {object} event - chat event
 * @returns {object} Response to send back to Chat
 */
function recordVote(event) {
  const parameters = event.common?.parameters;

  const choice = parseInt(parameters['index']);
  const userId = event.user.name;
  const state = JSON.parse(parameters['state']);

  // Add or update the user's selected option
  state.votes[userId] = choice;

  const card = buildVoteCard(state);
  return {
    thread: event.message.thread,
    actionResponse: {
      type: 'UPDATE_MESSAGE',
    },
    cards: [card],
  }
}

Vuelve a implementar la función:

gcloud functions deploy app --trigger-http --security-level=secure-always

Ahora deberías poder ejercer plenamente la app. Intenta invocar el comando /poll para proporcionar una pregunta y algunas opciones. Después de que envíes la tarjeta, aparecerá la tarjeta.

Emite tu voto y mira qué sucede.

Desde luego, no todas las encuestas son útiles. Por lo tanto, invita a algunos amigos o compañeros de trabajo a que lo prueben.

6. Felicitaciones

¡Felicitaciones! Compiló e implementó correctamente una aplicación de Google Chat con Cloud Functions. Si bien el codelab abarcaba muchos de los conceptos básicos para compilar una app, hay mucho más que explorar. Consulta los siguientes recursos y no olvides limpiar tu proyecto para evitar cargos adicionales.

Actividades adicionales

Si quieres explorar en más detalle la plataforma de Chat y esta app, puedes probar las siguientes opciones por tu cuenta:

  • ¿Qué sucede cuando @ mencionas la app? Intenta actualizar la app para mejorar el comportamiento.
  • La serialización del estado de la encuesta en la tarjeta es aceptable para espacios pequeños, pero tiene límites. Cambia a una opción mejor.
  • ¿Qué ocurre si el autor desea editar la encuesta o dejar de recibir votos nuevos? ¿Cómo implementarías esas funciones?
  • El extremo de la app todavía no está protegido. Agrega una verificación para asegurarte de que las solicitudes provengan de Google Chat.

Estas son solo algunas maneras de mejorar la app. Diviértete y usa tu imaginación.

Limpia

Sigue estos pasos a fin de evitar que se apliquen cargos a tu cuenta de Google Cloud Platform para los recursos que se usaron en este instructivo:

  • En Cloud Console, ve a la página Administrar recursos. En la esquina superior izquierda, haga clic en Menúícono de menú > IAM y administración > Administrar recursos.
  1. En la lista de proyectos, selecciona tu proyecto y haz clic en Borrar.
  2. En el cuadro de diálogo, escribe el ID del proyecto y, luego, haz clic en Cerrar para borrar el proyecto.

Más información

Para obtener más información sobre el desarrollo de apps de Chat, consulta:

Para obtener más información sobre el desarrollo en Google Cloud Console, consulta lo siguiente: