Build an interactive poll bot for Google Chat with Node.js

1. Introduction

Google Chat bots bring your services and resources right into Google Chat, letting users get information and take quick action without leaving the conversation.

In this codelab, you'll learn how to build and deploy a poll bot using Node.js and Cloud Functions.

Screenshot of bot

What you'll learn

  • Use the Cloud Shell
  • Deploy to Cloud Functions
  • Get user input with slash commands and dialogs
  • Create interactive cards

2. Setup and requirements

Create a Google Cloud project, then enable the APIs and services that the Chat bot will use

Prerequisites

Developing a Google Chat bot requires a Google Workspace account with access to Google Chat. If you don't already have a Google Workspace account, create one and sign in before proceeding with this codelab.

Self-paced environment setup

  1. Open the Google Cloud Console and create a project.

    The select a project menuThe new Project buttonThe Project ID

    Remember the project ID, a unique name across all Google Cloud projects (the name above has already been taken and will not work for you, sorry!). It will be referred to later in this codelab as PROJECT_ID.
  1. Next, in order to use Google Cloud resources, enable billing in Cloud Console.

Running through this codelab shouldn't cost much, if anything at all. Be sure to follow any instructions in the "Clean up" section at the end of the codelab which advises you how to shut down resources so you don't incur billing beyond this tutorial. New users of Google Cloud are eligible for the $300USD Free Trial program.

Google Cloud Shell

While Google Cloud can be operated remotely from your laptop, in this codelab we will use Google Cloud Shell, a command line environment running in Google Cloud.

Activate Cloud Shell

  1. From the Cloud Console, click Activate Cloud Shell The Cloud Shell icon.

    The Cloud Shell icon in the menu bar

    The first time you open Cloud Shell, you're presented with a descriptive welcome message. If you see the welcome message, click Continue. The welcome message doesn't appear again. Here's the welcome message:

    Cloud Shell welcome message

    It should only take a few moments to provision and connect to Cloud Shell. After connecting, you see the Cloud Shell Terminal:

    The Cloud Shell Terminal

    This virtual machine is loaded with all the development tools you need. It offers a persistent 5GB home directory and runs in Google Cloud, greatly enhancing network performance and authentication. All of your work in this codelab can be done with a browser or your Chromebook.Once connected to Cloud Shell, you should see that you are already authenticated and that the project is already set to your project ID.
  2. Run the following command in Cloud Shell to confirm that you are authenticated:
    gcloud auth list
    
    If you are prompted to authorize Cloud Shell to make a GCP API call, click Authorize.

    Command output
    Credentialed Accounts
    ACTIVE  ACCOUNT
    *       <my_account>@<my_domain.com>
    
    If your account is not selected by default, run:
    $ gcloud config set account <ACCOUNT>
    
  1. Confirm you have selected the correct project. In Cloud Shell, run:
    gcloud config list project
    
    Command output
    [core]
    project = <PROJECT_ID>
    
    If the correct project is not returned, you can set it with this command:
    gcloud config set project <PROJECT_ID>
    
    Command output
    Updated property [core/project].
    

As you complete this codelab, you'll use command line operations and edit files. For file editing, you can work with Cloud Shell's built-in code editor, Cloud Shell Editor, by clicking Open Editor on the right hand side of the Cloud Shell toolbar. Popular editors such as Vim and Emacs are also available in Cloud Shell.

3. Enable Cloud Functions, Cloud Build, and Google Chat APIs

From Cloud Shell, enable these APIs and services:

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

This operation may take a few moments to complete.

Once completed, a success message similar to this one appears:

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

4. Create the initial bot

Initialize the project

To begin, you'll create and deploy a simple "Hello world" bot. Bots are web services that respond to https requests and respond with a JSON payload. In this bot, you'll use Node.js and Cloud Functions.

In Cloud Shell, create a new directory named poll-bot and navigate to it:

mkdir ~/poll-bot
cd ~/poll-bot

All the remaining work for the codelab and the files you'll create will be in this directory.

Initialize the Node.js project:

npm init

NPM asks several questions about the project configuration, such as name and version. For each question, press ENTER to accept the default values. The default entry point is a file named index.js, which we'll create next.

Create the Chat bot backend

Time to start creating the bot. Create a file named index.js with the following content:

/**
 * Bot entry point.
 */
exports.bot = 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)
}

The bot won't do much yet, but that's OK. You'll add more functionality later.

Deploy the bot

To deploy the "Hello world" bot, you'll deploy the Cloud Function, configure the Chat bot in Google Cloud Console, and send a test message to the bot to verify deployment.

Deploy the Cloud Function

To deploy the "Hello world" bot's Cloud Function, enter the following command:

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

When finished, the output should look something like this:

availableMemoryMb: 256
buildId: 993b2ca9-2719-40af-86e4-42c8e4563a4b
buildName: projects/595241540133/locations/us-central1/builds/993b2ca9-2719-40af-86e4-42c8e4563a4b
entryPoint: bot
httpsTrigger:
  securityLevel: SECURE_ALWAYS
  url: https://us-central1-poll-bot-codelab.cloudfunctions.net/bot
ingressSettings: ALLOW_ALL
labels:
  deployment-tool: cli-gcloud
name: projects/poll-bot-codelab/locations/us-central1/functions/bot
runtime: nodejs14
serviceAccountEmail: poll-bot-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'

Note the URL of the deployed function in the httpsTrigger.url property. You'll use this in the next step.

Configure the bot

To configure the bot, go to the Chat configuration page in Cloud Console (APIs & Services > Dashboard > Hangouts Chat API > Configuration).

  1. In Bot name, enter "PollCodelab".
  2. In Avatar URL, enter https://raw.githubusercontent.com/google/material-design-icons/master/png/social/poll/materialicons/24dp/2x/baseline_poll_black_24dp.png.
  3. In Description, enter "Poll bot for codelab".
  4. nder Functionality, select Bot works in direct messages and Bot works in spaces with multiple users.
  5. Under Connection settings, select Bot URL and paste the URL for the Cloud Function (the httpsTrigger.url property from the last section).
  6. Under Permissions, select Specific people and groups in your domain and enter your email address.
  7. Click save.

The bot is now ready to message.

Test the bot

Before moving on, check that the bot is working by adding it to a space in Google Chat.

  1. Go to Google Chat.
  2. Next to Chat, click + > Find a bot.
  3. Enter "PollCodelab" in search.
  4. Click Chat.
  5. To message the bot, type "Hello" and press enter.

The bot should respond with a brief hello message.

Now that there's a basic skeleton in place, time to turn it in to something more useful!

5. Build the poll features

A quick overview of how the bot will work

The bot consists of two main parts:

  1. A slash command that displays a dialog for configuring the poll.
  2. An interactive card for voting and viewing results.

The bot also needs some state to store the poll configuration and results. This could be done with Firestore or any database, or state can be stored in the bot messages themselves. Since this bot is intended for quick informal polls of a team, storing the state in the bot messages works great for this use case.

The data model for the bot (expressed in Typescript) is:

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 };
}

Besides the topic or question and a list of choices, the state includes the author's id and name as well as the recorded votes. To prevent users from voting multiple times, votes are stored as a map of user ids to the index of their selected choice.

There are, of course, many different approaches but this provides a good starting point for running quick polls in a space.

Implement the poll configuration command

To let users start and configure polls, set up a slash command that opens a dialog. This is a multi-step process:

  1. Register the slash command that starts a poll.
  2. Create the dialog that sets up a poll.
  3. Let the bot recognize and handle the slash command.
  4. Create interactive cards that facilitate voting in the poll.
  5. Implement the code that lets the bot run polls.
  6. Redeploy the cloud function.

Register the slash command

To register a slash command, go back to the Chat configuration page in the console (APIs & Services > Dashboard > Hangouts Chat API > Configuration).

  1. Under Slash commands, click Add slash command.
  2. In Name, enter "/poll"
  3. In Command id, enter "1"
  4. In Description, enter "Start a poll".
  5. Select Opens a dialog.
  6. Click Done.
  7. Click Save.

The bot now recognizes the /poll command, and it opens a dialog. Next, let's configure the dialog.

Create the configuration form as a dialog

The slash command opens a dialog to configure the poll topic and possible choices. Create a new file called config-form.js with the following content:

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

This code generates the dialog form which lets the user set up the poll. It also exports a constant for the maximum number of choices a question can have. It's a good practice to isolate building the UI markup into stateless functions with any state passed in as parameters. It facilitates reuse, and later on this card will be rendered in different contexts.

This implementation also decomposes the card into smaller units or components. While not required, the technique is a best practice because it tends to be more readable and maintainable when building complex interfaces.s.

To see a sample of the complete JSON it builds, view it in the Card Builder tool.

Handle the slash command

Slash commands appear as MESSAGE events when sent to the bot. Update index.js to check for the presence of a slash command via a MESSAGE event and to respond with a dialog. Replace index.js with the following:

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

/**
 * Bot entry point.
 */
exports.bot = 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.',
        },
      },
    },
  }
}

The bot will now display a dialog with when the /poll command is invoked. Test the interaction by redploying the Cloud Function from Cloud Shell.

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

After the Cloud Function deploys, message the bot with the /poll command to test the slash command and dialog. The dialog sends a CARD_CLICKED event with the custom action start_poll. The event is handled in the updated entry point where it calls the startPoll method. For now, the startPoll method is stubbed out to just close the dialog. In the next section, you'll implement the voting functionality and connect all the parts together.

Implement the voting card

To implement the voting portion of the bot, start by defining the interactive card that provides an interface for people to vot.

Implement the vote interface

Create a file named vote-card.js with the following content:

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

The implementation is similar to the approach taken with the dialog, though the markup for interactive cards is slightly different than dialogs. As before, you can view a sample of the generated JSON in the Card Builder tool.

Implement the vote action

The voting card includes a button for each choice. The index of that choice, along with the serialized state of the poll, is attached to the button. The bot receives a CARD_CLICKED with the action vote along with any data attached to the button as parameters.

Update index.js with:

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

/**
 * Bot entry point.
 */
exports.bot = 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],
  }
}

The recordVote method parses the stored state and updates it with the user's vote, then rerenders the card. The poll results are serialized and stored with the card each time it is updated.

Connect the pieces

The bot is almost done. With the slash command implemented along with voting, the only thing left is to finish the startPoll method.

But, there's a catch.

When the poll configuration is submitted, the bot needs to perform two actions:

  1. Close the dialog.
  2. Post a new message to the space with the voting card.

Unfortunately, the direct reply to the HTTP request can only do one, and it must be the first one. To post the vote card, the bot must use the Chat API to create a new message asynchronously.

Add the client library

Run the following command to update the bot's dependencies to include the Google API client for Node.js.

npm install --save googleapis

Start the poll

Update index.js to the final version below:

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

/**
 * Bot entry point.
 */
exports.bot = 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],
  }
}

Redeploy the function:

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

You should now be able to fully exercise the bot. Try invoking the /poll command provide a question and a few choices. After submitting, the poll card appears.

Cast your vote and see what happens.

Of course polling yourself isn't all that useful, so invite some friends or coworkers to give it a try!

6. Congratulations

Congratulations! You've successfully built and deployed a Google Chat bot using Cloud Functions. While the codelab covered many of the core concepts for building a bot, there's a lot more to explore. See the resources below and don't forget to clean up your project to avoid additional charges.

Additional activities

If you'd like to explore the Chat platform and this bot in more depth, here are a few things you can try on your own:

  • What happens when you @ mention the bot? Try updating the bot to improve the behavior.
  • Serializing the poll state in the card is OK for small spaces, but has limits. Try switching to a better option.
  • What if the author wants to edit the poll, or stop taking new votes? How would you implement those features?
  • The bot endpoint isn't secured yet. Try adding some verification to ensure the requests are coming from Google Chat.

These are just a few different ways to improve the bot. Have fun and use your imagination!

Clean up

To avoid incurring charges to your Google Cloud Platform account for the resources used in this tutorial:

  • In the Cloud Console, go to the Manage resources page. Click At the top-left corner, click Menu menu icon > IAM & Admin > Manage Resources.
  1. In the project list, select your project then click Delete.
  2. In the dialog, type the project ID and then click Shut down to delete the project.

Learn more

For more information about developing Chat bots, see:

For more information about developing in Google Cloud Console, see: