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

1. Introdução

Os apps do Google Chat levam serviços e recursos diretamente para o Google Chat, para que os usuários obtenham informações e realizem ações rápidas sem sair da conversa.

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

Um app postando uma enquete perguntando aos participantes do espaço se os apps são incríveis e coletando votos com uma mensagem de card interativo.

O que você vai aprender

  • Como usar o Cloud Shell
  • Como implantar no Cloud Functions
  • Como obter entradas do usuário com comandos de barra e caixas de diálogo
  • Como criar cards interativos

2. Configuração e requisitos

Crie um projeto do Google Cloud e ative as APIs e os serviços que o app do Chat vai usar

Pré-requisitos

O desenvolvimento de um app do Google Chat requer 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 prosseguir com este codelab.

Configuração de ambiente autoguiada

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

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

    Lembre-se do ID do projeto, um nome exclusivo em todos os projetos do Google Cloud (o nome acima já foi usado e não funcionará para você). Faremos referência a ele mais adiante neste codelab como PROJECT_ID.
  1. Em seguida, para usar os recursos do Google Cloud, ative o faturamento no console do Cloud.

A execução deste codelab não será muito cara, se tiver algum custo. Não se esqueça de seguir todas as instruções da seção "Limpeza" no final do codelab, que orienta como encerrar recursos para não incorrer em cobranças além deste tutorial. Novos usuários do Google Cloud estão qualificados para o programa de US$ 300 de avaliação sem custos.

Google Cloud Shell

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

Ative o Cloud Shell

  1. No console do Cloud, clique em Ativar o Cloud Shell O ícone do Cloud Shell.

    O ícone do Cloud Shell na barra de menus

    Na primeira vez que abre o Cloud Shell, você vê uma mensagem de boas-vindas descritiva. Se a mensagem de boas-vindas aparecer, clique em Continuar. A mensagem de boas-vindas não vai aparecer de novo. Esta é a mensagem de boas-vindas:

    Mensagem de boas-vindas do Cloud Shell

    O provisionamento e a conexão com o Cloud Shell devem levar apenas alguns instantes. Após a conexão, o terminal do Cloud Shell aparece:

    O terminal do Cloud Shell

    Essa máquina virtual é 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 em um navegador ou no seu Chromebook. Após se conectar ao Cloud Shell, sua autenticação já está pronta e o projeto já está definido para o ID do projeto.
  2. Execute o seguinte comando no Cloud Shell para confirmar se a conta está autenticada:
    gcloud auth list
    
    Se você precisar autorizar o Cloud Shell a fazer uma chamada de API do GCP, clique em Autorizar.

    Resposta ao 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. Confirme 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, configure-o com este comando:
    gcloud config set project <PROJECT_ID>
    
    Resposta ao comando
    Updated property [core/project].
    

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

3. Ative APIs do Cloud Functions, Cloud Build e Google Chat

No Cloud Shell, ative as APIs e os serviços a seguir:

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

A conclusão dessa operação pode levar alguns instantes.

Depois de concluída, uma mensagem de sucesso semelhante a esta aparece:

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

4. Crie um app do Chat inicial

Inicialize o projeto

Para começar, você vai criar e implantar um app "Hello world" simples. Apps do Chat são serviços da Web que respondem a solicitações https com um payload JSON. Para este app, você vai usar o Node.js e o Cloud Functions.

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

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

Todo o trabalho restante do codelab e os arquivos que você criar vão ficar nesse diretório.

Inicialize o projeto Node.js:

npm init

O NPM faz várias perguntas sobre a configuração do projeto, como nome e 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 vamos criar a seguir.

Crie o back-end do app do Chat

É hora de começar a criar o app. Crie um arquivo nomeado como 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 faz muito, mas tudo bem. Você vai adicionar mais funcionalidades depois.

Implante o app

Para implantar o app "Hello world", implante a função do Cloud, configure o app do Chat no console do Google Cloud e envie uma mensagem de teste para o app para verificar a implantação.

Implante a função do Cloud

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

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

Quando terminar, a saída deve ser semelhante a 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'

Observe o URL da função implantada na propriedade httpsTrigger.url. Isso vai ser usado na próxima etapa.

Configure o app

Para configurar o app, acesse a página Configuração do Chat no console do Cloud.

  1. Desmarque Criar este app do Chat como um complemento do Workspace e clique em DESATIVAR para confirmar.
  2. No Nome do app, digite "PollCodelab".
  3. No URL do avatar, digite https://raw.githubusercontent.com/google/material-design-icons/master/png/social/poll/materialicons/24dp/2x/baseline_poll_black_24dp.png.
  4. Em Descrição, digite "App de pesquisa para o codelab".
  5. Em Funcionalidade, selecione Receber mensagens 1:1 e Participar de espaços e conversas em grupo.
  6. Em Configurações de conexão, selecione URL do endpoint HTTP e cole o URL da função do Cloud, a propriedade httpsTrigger.url da seção anterior.
  7. Em Permissões, selecione Pessoas e grupos específicos do seu domínio e digite seu endereço de e-mail.
  8. Clique em Salvar.

O app está pronto para enviar mensagens.

Teste o app

Antes de prosseguir, adicione o app a um espaço do Google Chat para conferir se ele está funcionando.

  1. Acesse Google Chat.
  2. Ao lado de Chat, clique em + > Encontrar apps.
  3. Digite "PollCodelab" na pesquisa.
  4. Clique em Chat.
  5. Para enviar mensagem para o app, digite "Hello" e aperte enter.

O app deve responder com uma breve mensagem "Hello".

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

5. Crie os recursos de enquete

Um breve resumo de como o app vai funcionar

O app tem duas partes principais:

  1. Um comando de barra que exibe uma caixa de diálogo para configuração da enquete.
  2. Um card interativo para votação e visualização dos 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. O estado pode ser armazenado nas próprias mensagens do app. Como esse app serve para enquetes informais rápidas de uma equipe, armazenar o 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 de IDs de usuário para o índice da escolha selecionada.

Existem muitas abordagens diferentes, mas essa fornece um bom ponto de partida para a execução de enquetes rápidas em um espaço.

Implemente 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 tem 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. Deixe o app reconhecer e processar o comando de barra.
  4. Crie cards interativos que facilitam a votação na enquete.
  5. Implemente o código que permite que o app execute enquetes.
  6. Implante a função do Cloud novamente.

Registre o comando de barra

Para registrar um comando de barra, volte para a página de Configuração do Chat no console (APIs e serviços > Painel > API Hangouts Chat > Configuração).

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

Agora o app reconhece o comando /poll e abre uma caixa de diálogo. Em seguida, configure a caixa de diálogo.

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

O comando de barra abre uma caixa de diálogo para configuração do tópico da enquete e das possíveis escolhas. Crie um novo arquivo chamado 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;

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

Essa implementação também decompõe o card em unidades ou componentes menores. Embora não seja necessá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 conferir uma amostra do JSON completo que ela cria, visualize-a na ferramenta Card Builder.

Gerencie o comando de barra

Comandos de barra aparecem como eventos MESSAGE quando enviados ao app. Atualize index.js para verificar a presença de um comando de barra por meio de 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.',
        },
      },
    },
  }
}

O app exibe uma caixa de diálogo quando o comando /poll é invocado. Implante novamente a função do Cloud pelo Cloud Shell para testar a interação.

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 onde chama o método startPoll. Por enquanto, o método startPoll foi eliminado apenas para fechar a caixa de diálogo. Na próxima seção, você vai implementar a funcionalidade de votação e conectar todas as partes.

Implemente o card de votação

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

Implemente 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 adotada com a caixa de diálogo, embora a marcação para cards interativos seja um pouco diferente. Como antes, é possível visualizar uma amostra do JSON gerado na ferramenta Card Builder.

Implemente a ação de voto

O card de votação tem um botão para cada escolha. 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 junto com todos os dados anexados ao botão como parâmetros.

Atualize o 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 o voto do usuário e, em seguida, renderiza novamente o card. Os resultados da enquete são serializados e armazenados com o card sempre que ele é atualizado.

Junte as peças

O app está quase pronto. Com o comando de barra implementado junto com a votação, só falta finalizar o método startPoll.

Mas há um problema.

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

  1. Fechar a caixa de diálogo.
  2. Postar uma nova mensagem no espaço com o card de votação.

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

Adicione a biblioteca de cliente

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

npm install --save googleapis

Inicie a enquete

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

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ê consegue usar totalmente o app. Tentar invocar o comando /poll fornece uma pergunta e algumas escolhas. Depois de enviar, o card da enquete aparece.

Dê seu voto e veja o que acontece.

Fazer enquetes só com uma pessoa não é tão útil, então convide alguns amigos ou colegas de trabalho.

6. Parabéns

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

Atividades complementares

Se você quiser explorar mais a plataforma do Chat e esse app, pode tentar algumas das opções a seguir 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. Tente mudar 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 está protegido. Tente adicionar alguma verificação para garantir que as solicitações sejam provenientes do Google Chat.

Essas são algumas maneiras diferentes de melhorar o app. Divirta-se e use sua imaginação.

Limpeza

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: No canto superior esquerdo, clique em Menu ícone de menu > IAM e administrador > Gerenciar recurso.
  1. Na lista de projetos, selecione seu projeto e clique em Excluir.
  2. Na caixa de diálogo, digite o ID do projeto e clique em Encerrar para excluí-lo.

Saiba mais

Para obter mais informações sobre como desenvolver apps do Chat, consulte:

Para obter mais informações sobre como desenvolver no console do Google Cloud, consulte: