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 y también implementar una app de encuestas con Node.js y Cloud Functions.

Una app publica una encuesta en la que pregunta a los miembros del espacio si las apps son geniales y recopila votos con un mensaje de tarjeta interactiva.

Qué aprenderás

  • Cómo usar Cloud Shell
  • Cómo realizar implementaciones en Cloud Functions
  • Cómo obtener entradas del usuario con comandos de barra y diálogos
  • Cómo crear tarjetas interactivas

2. Configuración y requisitos

Crea un proyecto de Google Cloud y, luego, habilita las APIs y los servicios que usará la app 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 de autoaprendizaje

  1. Abre la consola de Google Cloud y crea un proyecto.

    El menú para seleccionar un proyectoEl botón Nuevo proyectoEl ID del proyecto

    Recuerda 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, habilita la facturación en la consola de Cloud para usar los recursos de Google Cloud.

Ejecutar este codelab no debería costar mucho, tal vez nada. Asegúrate de seguir todas las instrucciones de la sección “Realiza una limpieza” al final del codelab, en la que se indica cómo cerrar los recursos para no incurrir en facturaciones 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 manera remota desde tu laptop, en este codelab usaremos Google Cloud Shell, un entorno de línea de comandos que se ejecuta en Google Cloud.

Activa Cloud Shell

  1. En la consola de Cloud, 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 lo ves, haz clic en Continuar (Continue). 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 un momento. Después de conectarte, verás la terminal de Cloud Shell:

    La terminal de Cloud Shell

    Esta máquina virtual está cargada con todas las herramientas de desarrollo necesarias. 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. Todo tu trabajo en este codelab se puede hacer con un navegador o tu Chromebook. Cuando te conectes a Cloud Shell, deberías ver que ya se hizo la autenticación y que el proyecto ya tiene establecido el ID del proyecto.
  2. En Cloud Shell, ejecuta el siguiente comando para confirmar que tienes la autenticación:
    gcloud auth list
    
    Si se te solicita que autorices a Cloud Shell para que realice 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 el siguiente comando:
    $ gcloud config set account <ACCOUNT>
    
  1. Confirma que ya seleccionaste el proyecto correcto. En Cloud Shell, ejecuta este comando:
    gcloud config list project
    
    Resultado del comando
    [core]
    project = <PROJECT_ID>
    
    Si no aparece el proyecto correcto, puedes establecerlo con este comando:
    gcloud config set project <PROJECT_ID>
    
    Resultado del comando
    Updated property [core/project].
    

A medida que completes este codelab, usarás operaciones de la línea de comandos y editarás archivos. Para editar archivos, puedes trabajar con el editor de código integrado de Cloud Shell, el Editor de Cloud Shell. Para usarlo, haz clic en Abrir Editor en el lado derecho de la barra de herramientas de Cloud Shell. En Cloud Shell, también hay disponibles editores populares como Vim y Emacs.

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

En Cloud Shell, habilita estas APIs y servicios:

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

Es posible que esta operación se tarde unos minutos en completarse.

Cuando se complete, aparecerá un mensaje de éxito similar a este:

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

4. Crea la app de Chat inicial

Inicializa el proyecto

Para comenzar, crearás una app sencilla de “Hello World” y la implementarás. Las apps de Chat son servicios web que responden a solicitudes HTTP y responden con una carga útil de 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

Todo el trabajo restante de este codelab y los archivos que crearás se encontrarán en este directorio.

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

Llegó el momento 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 todavía, pero no te preocupes. Podrás agregarle más funciones más adelante.

Implementa la app

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

Implementa 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 termines, el resultado debería ser similar a esto:

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'

Anota 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, dirígete a la página configuración de Chat en la consola de Cloud.

  1. Desmarca Crea esta app de Chat como complemento de Workspace y haz clic en INHABILITAR para confirmar.
  2. En Nombre de la app, escribe “PollCodelab”.
  3. En URL del avatar, escribe https://raw.githubusercontent.com/google/material-design-icons/master/png/social/poll/materialicons/24dp/2x/baseline_poll_black_24dp.png.
  4. En Descripción, escribe “App de encuestas para codelab”.
  5. En Funcionalidad, selecciona Recibir mensajes 1:1 y Unirse a espacios y conversaciones grupales.
  6. En Configuración de la conexión, selecciona URL del extremo HTTP y pega la URL de la Cloud Function (la propiedad httpsTrigger.url de la sección anterior).
  7. En Permisos, selecciona Personas y grupos específicos de tu dominio y escribe tu dirección de correo electrónico.
  8. Haz clic en Guardar.

La app ya está lista para enviar mensajes.

Prueba la app

Antes de avanzar, agrega la app a un espacio en Google Chat para verificar que esté funcionando.

  1. Ve a Google Chat.
  2. Junto a Chat, haz clic en + > Buscar apps.
  3. Escribe “PollCodelab” en la búsqueda.
  4. Haz clic en Chat.
  5. Para enviar un mensaje a la app, escribe “Hola” y presiona enviar.

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

Ahora que hay una estructura básica, es momento de convertir la app en una herramienta más útil.

5. Compila las funciones de encuesta

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 además necesita cierto estado para almacenar la configuración y los resultados de la encuesta. Esto se puede hacer con Firestore o cualquier base de datos, o el estado se puede almacenar en los mismos mensajes de la app. Ya que esta app está destinada para encuestas rápidas informales de un equipo, almacenar el estado en los mensajes de la app funciona de forma excelente para este caso de uso.

Este es el modelo de datos de la app (expresado en Typescript):

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 y los votos registrados. Para prevenir que los usuarios voten más de una vez, los votos se almacenan como un mapa de IDs de usuarios según el índice de su opción seleccionada.

Por supuesto que existen muchos enfoques diferentes, pero este brinda un buen punto de partida para ejecutar encuestas rápidas en un espacio.

Implementa el comando de configuración de la encuesta

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

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

Registra el comando de barra

Para registrar un comando de barra, vuelve a la página de configuración de Chat en la consola (APIs y servicios > Panel > API de Hangouts Chat > Configuración).

  1. En Comandos de barra, haz clic en Agregar un comando de barra nuevo.
  2. En Nombre, escribe “/poll”.
  3. En ID de comando, escribe “1”.
  4. En Descripción, escribe “Iniciar una encuesta”.
  5. Selecciona Abrir un diálogo.
  6. Haz 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 nuevo archivo 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 permite que el usuario configure la encuesta. También exporta una constante para el número máximo de opciones que una pregunta puede tener. Recomendamos aislar la compilación del lenguaje de marcado de la IU en funciones sin estado con cualquier estado pasado como parámetros. Esto facilita la reutilización y, más adelante, 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, esta técnica es una práctica recomendada porque suele ser más legible y fácil de mantener cuando se compilan interfaces complejas.

Para ver una muestra del JSON completo que compila, puedes verlo en la herramienta Card Builder.

Controla el comando de barra

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

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 el momento en que se invoca el comando /poll. Prueba la interacción volviendo a implementar la Cloud Function desde Cloud Shell.

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

Después de que se implemente la Cloud Function, envía 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 está incluido como stub solo para cerrar el diálogo. En la siguiente 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 definiendo la tarjeta interactiva que proporciona una interfaz para que las personas voten.

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 que se usó en el diálogo, aunque que la marca de las tarjetas interactivas es un poco diferente a los diálogos. Al igual que antes, puedes ver una muestra del JSON generado en la herramienta Card Builder.

Implementa la acción para votar

La tarjeta para votar incluye un botón para cada opción. El índice de esa opción, junto con el estado serializado de la encuesta, está adjunto en el botón. La app recibe un CARD_CLICKED con la acción vote junto con los datos adjuntos al botón como parámetros.

Actualiza index.js con este código:

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, lo actualiza con el voto del usuario y, luego, renderiza la tarjeta. Los resultados de la encuesta se serializan y almacenan con la tarjeta cada vez que se actualiza.

Conecta las partes

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

Pero hay una traba.

Cuando se envíe la configuración de la encuesta, la app debe realizar dos acciones:

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

Desafortunadamente, la respuesta directa a la solicitud HTTP solo puede hacer una de las dos, y debe ser la primera. Si quieres publicar la tarjeta de votación, la app debe usar la API de Chat para crear un nuevo mensaje de manera asíncrona.

Agrega la biblioteca cliente

Ejecuta el siguiente comando para actualizar las dependencias de la app para que incluyan al cliente de la API de Google para Node.js.

npm install --save googleapis

Inicia la encuesta

Actualiza index.js a la versión final que se indica 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 implementar por completo la app. Prueba invocar el comando /poll para proporcionar una pregunta y algunas opciones. Después de enviarlas, aparecerá la tarjeta de encuesta.

Vota y ve lo que ocurre.

Por supuesto que encuestarte a ti mismo no es muy útil, así que invita a algunos amigos o compañeros de trabajo para que lo intenten.

6. Felicitaciones

¡Felicitaciones! Usaste Cloud Functions para compilar y, luego, implementar una app de Google Chat de manera correcta. Si bien en el codelab se abordaron muchos de los conceptos fundamentales para desarrollar una app, hay mucho más por explorar. Consulta los recursos que están más abajo y no te olvides de limpiar tu proyecto para evitar cargos adicionales.

Actividades adicionales

Si quieres explorar con mayor profundidad la plataforma de Chat y esta app, puedes intentar estas acciones:

  • ¿Qué ocurre cuando mencionas con @ a la app? Actualiza la app para mejorar el comportamiento.
  • ESTÁ BIEN serializar el estado de la encuesta en la tarjeta en espacios pequeños, pero hay límites. Cámbiate a una mejor opción.
  • ¿Qué ocurre si el autor quiere editar la encuesta o dejar de recibir votos nuevos? ¿Cómo implementarías esas funciones?
  • El extremo de la app todavía no está asegurado. Agrega verificaciones para asegurarte de que las solicitudes provengan de Google Chat.

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

Realiza una limpieza

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

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

Más información

Si quieres obtener más información para desarrollar apps de Chat, consulta estos recursos:

Si quieres obtener más información para desarrollar en la consola de Google Cloud, consulta estos recursos: