Criar um app de enquete interativo para o Google Chat com Node.js

1. Introdução

Com os apps Google Chat, seus serviços e recursos são transferidos diretamente para o Google Chat. Assim os usuários recebem informações e podem realizar ações rapidamente sem sair da conversa.

Neste codelab, você aprenderá a criar e implantar um app de enquete usando o Node.js e o Cloud Functions.

O que você aprenderá

  • Usar o Cloud Shell
  • Implantar no Cloud Functions
  • Receber entrada do usuário com comandos de barra e caixas de diálogo
  • Como criar cards interativos

2. Configuração e requisitos

Criar um projeto do Google Cloud e ativar as APIs e os serviços que o app Chat usará

Pré-requisitos

Para desenvolver um app Google Chat, você precisa de uma conta do Google Workspace com acesso ao Google Chat. Se você ainda não tem uma conta do Google Workspace, crie uma e faça login antes de continuar com este codelab.

Configuração de ambiente personalizada

  1. Abra o Console do Google Cloud e crie um projeto.

    O menu de seleção de um projetoO novo botão "Projeto"O ID do projeto

    Lembre-se do ID do projeto, um nome exclusivo em todos os projetos do Google Cloud. O nome acima já está em uso e não funcionará para você. Ele será indicado posteriormente neste codelab como PROJECT_ID.
  1. Em seguida, para ativar os recursos do Google Cloud, ative o faturamento no Console do Cloud.

A execução deste codelab não será muito cara, se for o caso. Siga as instruções da seção "Limpeza", no final do codelab, para saber como encerrar recursos. Assim, você não receberá cobranças além deste tutorial. Novos usuários do Google Cloud estão qualificados para o programa de avaliação gratuita de US$ 300.

Google Cloud Shell

Embora o Google Cloud possa ser operado remotamente do seu laptop, neste codelab, usaremos o Google Cloud Shell, um ambiente de linha de comando executado no Google Cloud.

Ativar o Cloud Shell

  1. No Console do Cloud, clique em Ativar o Cloud Shell Ícone do Cloud Shell.

    Ícone do Cloud Shell na barra de menus

    Na primeira vez que você abrir o Cloud Shell, uma mensagem de boas-vindas descritiva será exibida. Se você vir a mensagem de boas-vindas, clique em Continuar. A mensagem de recepção não aparece novamente. Esta é a mensagem de recepção:

    Mensagem de recepção do Cloud Shell

    Leva apenas alguns minutos para provisionar e se conectar ao Cloud Shell. Depois de se conectar, você verá o Terminal do Cloud Shell:

    O terminal do Cloud Shell

    Esta máquina virtual está carregada com todas as ferramentas de desenvolvimento necessárias. Ela oferece um diretório principal persistente de 5 GB, além de ser executada no Google Cloud. Isso aprimora o desempenho e a autenticação da rede. Todo o trabalho neste codelab pode ser feito com um navegador ou seu Chromebook.Depois de se conectar ao Cloud Shell, você já estará autenticado e o projeto já estará definido com seu ID.
  2. Execute o seguinte comando no Cloud Shell para confirmar se a conta está autenticada:
    gcloud auth list
    
    Se for necessário autorizar o Cloud Shell a fazer uma chamada de API do GCP, clique em Autorizar.

    Saída de comando
    Credentialed Accounts
    ACTIVE  ACCOUNT
    *       <my_account>@<my_domain.com>
    
    Se sua conta não estiver selecionada por padrão, execute:
    $ gcloud config set account <ACCOUNT>
    
  1. Verifique se você selecionou o projeto correto. No Cloud Shell, execute:
    gcloud config list project
    
    Resposta ao comando
    [core]
    project = <PROJECT_ID>
    
    Se o projeto correto não for retornado, defina-o com este comando:
    gcloud config set project <PROJECT_ID>
    
    Resposta ao comando
    Updated property [core/project].
    

Ao concluir este codelab, você usará operações de linha de comando e editará arquivos. Para editar arquivos, você pode trabalhar com o editor de código integrado do Cloud Shell, Editor do Cloud Shell, clicando em Abrir editor no lado direito da barra de ferramentas. Editores conhecidos como o Vim e o Emacs também estão disponíveis no Cloud Shell.

3. Ativar as APIs Cloud Functions, Cloud Build e Google Chat

No Cloud Shell, ative estas APIs e serviços:

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

Essa operação pode levar alguns instantes para ser concluída.

Quando terminar, você verá uma mensagem de sucesso semelhante a esta:

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

4. Criar o app Chat inicial

Inicializar o projeto

Para começar, crie e implante um app "Hello world" simples. Os apps de chat são serviços da Web que respondem a solicitações https com um payload JSON. Para este app, você usará o Node.js e o Cloud Functions.

No Cloud Shell, crie um diretório chamado poll-app e navegue até ele:

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

Todo o trabalho restante para o codelab e os arquivos que você criará estarão nesse diretório.

Inicialize o projeto Node.js:

npm init

O NPM faz várias perguntas sobre a configuração do projeto, como o nome e a versão. Para cada pergunta, pressione ENTER para aceitar os valores padrão. O ponto de entrada padrão é um arquivo chamado index.js, que será criado em seguida.

Criar o back-end do app Chat

Hora de começar a criar o app. Crie um arquivo chamado index.js com o seguinte conteúdo:

/**
 * 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)
}

O app ainda não fará muito, mas isso não é um problema. Você adicionará mais funcionalidades posteriormente.

Implantar o aplicativo

Para implantar o aplicativo "Hello World", implante o Cloud Functions, configure o aplicativo do Chat no Console do Google Cloud e envie uma mensagem de teste ao aplicativo para verificar a implantação.

Implante a Função do Cloud

Para implantar a função do Cloud "Hello World" do aplicativo, digite o seguinte comando:

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

Quando terminar, a resposta será parecida com esta:

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'

Anote o URL da função implantada na propriedade httpsTrigger.url. Você usará isso na próxima etapa.

Configurar o app

Para configurar o app, acesseConfiguração do Chat no Console do Cloud (APIs e serviços >Painel >API Chat >Configuração .

  1. Em Nome do app, insira "PollCodelab".
  2. Em URL do avatar, insira https://raw.githubusercontent.com/google/material-design-icons/master/png/social/poll/materialicons/24dp/2x/baseline_poll_black_24dp.png.
  3. Em Descrição, digite "Enquete do codelab".
  4. Em Funcionalidade, selecione O app pode receber mensagens diretamente e O app funciona em espaços com vários usuários.
  5. Em Configurações de conexão, selecione URL do aplicativo e cole o URL da função do Cloud (a propriedade httpsTrigger.url da última seção).
  6. Em Permissões, selecione Pessoas e grupos específicos no seu domínio e digite seu endereço de e-mail.
  7. Clique em Salvar.

Agora o app está pronto para enviar mensagens.

Testar o app

Antes de continuar, verifique se o app está funcionando adicionando-o a um espaço no Google Chat.

  1. Acessar o Google Chat.
  2. Ao lado de "Chat", clique em + > Encontrar apps.
  3. Digite "PollCodelab" na pesquisa.
  4. Clique em Chat.
  5. Para enviar uma mensagem ao app, digite "Hello" e pressione Enter.

O app responderá com uma breve mensagem de boas-vindas.

Agora que já existe um esqueleto básico, é hora de transformá-lo em algo mais útil.

5. Criar os recursos de enquete

Uma visão geral rápida de como o app funcionará

O app consiste em duas partes principais:

  1. Um comando de barra que exibe uma caixa de diálogo para configurar a enquete.
  2. Um card interativo para votar e ver os resultados.

O app também precisa de algum estado para armazenar a configuração e os resultados da enquete. Isso pode ser feito com o Firestore ou qualquer banco de dados ou o estado pode ser armazenado nas próprias mensagens do app. Como este app é destinado a enquetes informais rápidas de uma equipe, o armazenamento do estado nas mensagens do app funciona muito bem para esse caso de uso.

O modelo de dados do app (expresso em 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 };
}

Além do tópico ou da pergunta e de uma lista de opções, o estado inclui o ID e o nome do autor, bem como os votos registrados. Para evitar que os usuários votem várias vezes, os votos são armazenados como um mapa dos IDs de usuário para o índice da escolha escolhida.

Existem muitas abordagens diferentes, mas isso é um bom ponto de partida para a realização de enquetes rápidas em um espaço.

Implementar o comando de configuração de enquete

Para permitir que os usuários iniciem e configurem enquetes, configure um comando de barra que abra uma caixa de diálogo. Esse processo é composto por várias etapas:

  1. Registre o comando de barra que inicia uma enquete.
  2. Crie a caixa de diálogo que configura uma enquete.
  3. Permita que o app reconheça e processe o comando de barra.
  4. Crie cards interativos para facilitar a votação na enquete.
  5. Implementar o código que permite que o app execute enquetes.
  6. Implante novamente a Função do Cloud.

Registrar o comando de barra

Para registrar um comando de barra, volte para oConfiguração do Chat no Console (APIs e serviços >Painel >API Chat >Configuração .

  1. Em Comandos de barra, clique em Adicionar comando de barra.
  2. Em Nome, digite "/poll"
  3. Em Código do comando, digite "1".
  4. Em Descrição, digite "Start a enquete".
  5. Selecione Abre uma caixa de diálogo.
  6. Clique em Concluído.
  7. Clique em Salvar.

O app agora reconhece o comando /poll e abre uma caixa de diálogo. Agora, vamos configurar a caixa de diálogo.

Criar o formulário de configuração como uma caixa de diálogo

O comando de barra abre uma caixa de diálogo para configurar o tópico da enquete e as possíveis escolhas. Crie um novo arquivo com o nome config-form.js com o seguinte conteúdo:

/** 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;

Esse código gera o formulário de caixa de diálogo que permite ao usuário configurar a enquete. Ela também exporta uma constante para o número máximo de opções que uma pergunta pode ter. É recomendável isolar a criação da marcação da IU em funções sem estado com qualquer estado transmitido como parâmetros. Ele facilita a reutilização e, mais tarde, esse card será renderizado em diferentes contextos.

Essa implementação também decompõe o cartão em unidades ou componentes menores. Embora não seja obrigatória, a técnica é uma prática recomendada porque tende a ser mais legível e fácil de manter ao criar interfaces complexas.

Para ver um exemplo do JSON completo criado por ele, acesse a ferramenta Card Builder.

Processar o comando de barra

Os comandos de barra aparecem como eventos MESSAGE quando enviados ao app. Atualize index.js para verificar a presença de um comando de barra em um evento MESSAGE e responder com uma caixa de diálogo. Substitua index.js pelo seguinte:

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.',
        },
      },
    },
  }
}

Agora, o app exibirá uma caixa de diálogo quando o comando /poll for invocado. Teste a interação usando a Função do Cloud no Cloud Shell.

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

Após a implantação da Função do Cloud, envie uma mensagem ao app com o comando /poll para testar o comando de barra e a caixa de diálogo. A caixa de diálogo envia um evento CARD_CLICKED com a ação personalizada start_poll. O evento é processado no ponto de entrada atualizado em que ele chama o método startPoll. Por enquanto, o método startPoll está fragmentado para fechar a caixa de diálogo. Na próxima seção, você implementará o recurso de votação e conectará todas as partes.

Implementar o card de votação

Para implementar a parte de votação do app, comece definindo o card interativo que oferece uma interface para as pessoas votarem.

Implementar a interface de voto

Crie um arquivo chamado vote-card.js com o conteúdo a seguir:

/**
 * 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;

A implementação é semelhante à abordagem feita com a caixa de diálogo, embora a marcação para cards interativos seja um pouco diferente das caixas de diálogo. Como antes, veja uma amostra do JSON gerado na ferramenta Card Builder.

Implementar a ação de voto

O card de votação inclui um botão para cada opção. O índice dessa escolha, junto com o estado serializado da enquete, é anexado ao botão. O app recebe um CARD_CLICKED com a ação vote e todos os dados anexados ao botão como parâmetros.

Atualizar index.js com:

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],
  }
}

O método recordVote analisa o estado armazenado e o atualiza com a votação do usuário. Em seguida, ele renderiza novamente o card. Os resultados da enquete são serializados e armazenados com o card a cada atualização.

Conecte as peças

O app está quase pronto. Com o comando de barra implementado durante a votação, o único fator restante é concluir o método startPoll.

Mas há um problema.

Quando a configuração da enquete é enviada, o app precisa executar duas ações:

  1. Feche a caixa de diálogo.
  2. Poste uma nova mensagem no espaço com o cartão de votação.

Infelizmente, a resposta direta à solicitação HTTP pode fazer apenas uma, e precisa ser a primeira. Para postar o card de votação, o app precisa usar a API Chat para criar uma nova mensagem de forma assíncrona.

Adicionar a biblioteca de cliente

Execute o comando a seguir para atualizar as dependências do app e incluir o cliente de API do Google para Node.js.

npm install --save googleapis

Iniciar a enquete

Atualize index.js para a versão final abaixo:

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],
  }
}

Implante a função novamente:

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

Agora, você poderá exercer totalmente o aplicativo. Tente invocar o comando /poll para fornecer uma pergunta e algumas opções. Após o envio, o card de enquete será exibido.

Vote e veja o que acontece.

Obviamente, essa pesquisa não é tão útil. Convide alguns amigos ou colegas de trabalho para experimentar.

6. Parabéns

Parabéns! Você criou e implantou um app Google Chat usando o Cloud Functions. Embora o codelab tenha abordado muitos dos principais conceitos para criar um app, há muito mais para explorar. Veja os recursos abaixo e não se esqueça de limpar o projeto para evitar cobranças adicionais.

Outras atividades

Se quiser conhecer melhor a plataforma de chat e o app, veja o que você pode fazer por conta própria:

  • O que acontece quando você menciona @ o app? Tente atualizar o app para melhorar o comportamento.
  • A serialização do estado da enquete no card é aceitável para espaços pequenos, mas tem limites. Mude para uma opção melhor.
  • E se o autor quiser editar a enquete ou parar de receber novos votos? Como você implementaria esses recursos?
  • O endpoint do app ainda não é protegido. Tente fazer uma verificação para garantir que as solicitações estejam vindo do Google Chat.

Estas são apenas algumas maneiras diferentes de melhorar o app. Divirta-se e use a imaginação!

Limpar

Para evitar que os recursos usados nesse tutorial sejam cobrados na sua conta do Google Cloud Platform:

  • No Console do Cloud, acesse a página Gerenciar recursos: Clique no canto superior esquerdo, clique em Menuícone de menu > IAM e administrador > Gerenciar recursos.
  1. Na lista de projetos, selecione seu projeto e clique em Excluir.
  2. Na caixa de diálogo, digite o código do projeto e clique em Encerrar para excluí-lo.

Saiba mais

Para saber mais sobre o desenvolvimento de apps do Chat, consulte estes recursos:

Para mais informações sobre o desenvolvimento no Console do Google Cloud, consulte: