Pizza ordering app with Google Cloud Functions

The Ably Realtime service provides a “pub/sub” (publish/subscribe) messaging system with which you can build sophisticated workflows and dataflow pipelines to meet the needs of your application. This can be as simple as publishing messages to named channels of your choice, and having users and server processes subscribe to those channels and respond with further messages as needed. But what if you don’t have a server to coordinate all of this, or simply don’t want to manage that side of things?

Even though Ably provides a full suite of authentication and authorization features to make sure users can only do what they’re permitted to do, we still need a way to connect your messaging workflow to your business logic – something that would normally happen on your application’s server. Doing so from a client-side browser application is risky, as it means exposing your business logic and other potentially sensitive data and services to the user.

With the rise of serverless solutions such as Google Cloud Functions, the problem goes away. Create server functions that contain your business logic, credentials and other sensitive data, hook them up to your application, and all of the sensitive work is hidden away from the user, not to mention being managed by world-class infrastructure providers such as Amazon, Google or Microsoft.

Ably’s Webhooks feature provides a way to define custom criteria that will automatically trigger different kinds of actions in response to channel messages, channel lifecycle events (such as a channel being created) and presence events (such as users entering or leaving a channel), and is how we’ll integrate with your server functions without exposing them to your users.

What we’ll be building

For this tutorial, we’ll be using Webhooks events in response to incoming messages in order to automatically invoke your own Google functions. We’re going to build a simple pizza ordering system with a virtual pizza assistant chat bot. When a new customer hits your website, they’ll be assigned a random ID, and their messages will be published to a private channel created just for that customer. A webhook triggered by those messages will call your Google functions to invoke the pizza chat bot to take their order. Using the Ably REST API, it will post its responses back on another channel that the web application subscribes to.

To get you up and running quickly, we’ve implemented a simple JavaScript-based pizza chat bot and made it available via this tutorial’s repository. The chat bot helps each customer design their own pizza, keep track of the order price, and then finalize the pizza order when they’re finished. The nlp compromise npm package is used for basic language parsing, and fuzzyset.js gives the bot the ability to cope with spelling mistakes, typographical errors and other textual variations it might encounter.

The details of the chat bot’s logic are beyond the scope of this tutorial — you’ll simply be referencing it from your Google function. You are welcome to explore the chat bot’s source code (see pizza.js in the tutorial repository) and modify it as you see fit. Note also that while the script implies that a completed order has been placed, it is left as an optional exercise to actually dispatch the order to an endpoint, such as an Ably Queues that you might have configured for processing orders. For more sophisticated natural language capabilities, we’d recommend checking out Google’s Dialogflow.


Diagram of the tutorial project's architecture


Animated image of the browser app in action

Step 1: Ably API key

We’re going to need an API key for use by your browser application, and for the Google function to allow it to post a response back to Ably. For our purposes, only one API key is required. If you extend the tutorial with your own functionality, you may wish to create a separate API key for the Google function if you need it to have a different set of permissions than those assigned for use by the browser app.

First, log into your Ably dashboard, find the app you’re going to be using for this tutorial (the “Sandbox” app is probably fine), then click “Manage App”.


Manage your Ably app

Select the “API Keys” tab, and click “Create a new API key”:


Create a new API key

  1. Enter a name for your API key
  2. Select only the “publish” and “subscribe” options
  3. Select “Selected channels and queues”, and enter the channel prefix pizza:* to ensure that the API key can only access channels that start with pizza:.
  4. Click the “Create key” button


Fill out the API key form

Scroll down if necessary, and copy and paste your new API key to a text file for later use in your application.


Copy the API key

Step 2: Creating your function

Create a folder on your local machine, then navigate to that folder from your console or terminal. Create a new package.json file using npm init. Next, install the project dependencies using npm install ably compromise fuzzyset. Once you have done this, your package.json file will look something like this:

{
  "name": "ably-pizza-tutorial",
  "version": "1.1.0",
  "description": "Ably Function Tutorial",
  "dependencies": {
    "ably": ">=1.2",
    "compromise": "^11.2.1",
    "fuzzyset": "0.0.4"
  }
}

Note that while the above package.json file can be found in the tutorial repository, the package versions it references may become outdated if we haven’t updated the tutorial repository in a while (particularly the ably package, which is updated more frequently than the tutorial repository). We recommend performing the install step above, which will ensure that you are referencing up-to-date package versions.

From the tutorial repository, download the pizza.js file, and place it in your project folder alongside package.json.

Create a new file in your project folder, and name it index.js. Copy the following script into the file, making sure to update the line containing “YOUR_API_KEY_HERE” with your actual API key:

const Ably = require('ably');
const processMessage = require('./pizza');

exports.invokePizzaAssistant = function (req, res) {
  console.log(req.body);
  if (req.body && req.body.customerId) {
    const message = req.body.message;
    const context = req.body.context;
    const customerId = req.body.customerId;

    // Invoke the pizza assistant chat bot's script to process the message that
    // the customer will be typing into your web application's chat interface:
    const response = processMessage(req.body.message, context);

    // Note that we use uses Ably.Rest here, not Realtime, because we don't want
    // to start a websocket connection to Ably just to publish a single response;
    // doing so would be inefficient.
    const ably = new Ably.Rest('YOUR_API_KEY_GOES_HERE');

    // Get the pizza bot's response channel
    const channel = ably.channels.get('pizza:bot:' + customerId);

    // Publish the response, and handle any unexpected errors
    channel.publish('bot', response, err => {
      if (err) {
        console.log(err); // Use the Azure logging console to see this output
        res.status(500).send('Error publishing response back to Ably');
      }
      else {
        res.status(200).send('OK');
      }
    });
  }
  else {
    res.status(400).send('Invalid request format');
  }
};

Note: you can test and run your cloud function on your local machine, using the cloud functions emulator. It is actually the recommended way to develop cloud functions, especially seeing as Google’s interface for directly editing your functions is somewhat limited. Follow the instructions at the above link for further details.

Finally, create a ZIP package containing your project files, ready for deployment as described further below.

Step 3: Google Cloud project setup

Make sure you have a Google Cloud account with which to create your server function. If you don’t have a Google Cloud account, create a free one here.

  1. Once you’re logged into your Google Cloud dashboard, click the “Select a project” option in the top left of the title bar.
  2. A white dialog box will be displayed. To the right of the search box at the top of the dialog area, click the “Create project” button, displayed as a “+” symbol.
  3. Give your project a name. Take a note of the project ID – you will need it when setting up the Ably Integrations rule.
  4. There will be a short waiting period while your project is created and associated with your account. When your project has been created, you’ll be taken to your project dashboard. From the left menu, look under the “Compute” subheading and select the “Cloud Functions” option. (You may be required to enable the API if you’re a first-time Cloud Functions user. Prompts are provided in the user interface to help you do this.)

Note: For some new users, the “Cloud Functions” option may be missing from the menu. If so, select the “APIs & services” option higher up in the menu. Next, perform a search for “Functions” and follow the prompts to enable the Cloud Functions API for your account. Once you’ve done that, return to your project dashboard, and the menu item should be available.


Create a new function app

Step 4: Deployment

To complete this step, you’ll need the deployment package ZIP file from step 2, above.

Upon selecting the “Cloud Functions” menu item, follow the prompt to create a function. You’ll be presented with a form that you should fill out as follows:

  1. Enter a name for your function. Something descriptive is ok, but it is convenient to simply use the name of the main function that will be invoked in your deployment package (see #6, below).
  2. Take note of the function URL, as you’ll need it later when creating your webhooks.
  3. Under the “Source Code” heading, select “ZIP upload”.
  4. Browse and select your deployment package from your local machine.
  5. Select a bucket to upload your package to. A bucket is simply a folder in your Google Cloud account. If you don’t have a bucket ready to go, follow the instructions and prompts provided in the user interface.
  6. Enter the name of the function to execute. This should be the name of the function implemented in step 2, above. We used invokePizzaAssistant for this tutorial; if you named yours differently, use your chosen function name instead.
  7. Finally, click the blue “Create” button, and wait while your function is deployed.


Define function app details

When your function has been deployed successfully, it’s time to test it and make sure everything’s working correctly. Ably provides features for testing and monitoring channel activity.

  1. Log into your Ably account and go to the dashboard for your app.
  2. From the “Dev Console” tab, scroll to the bottom and in the field labelled “Enter a channel name”, enter pizza:bot:test, then click “Attach to channel”.
  3. The channel will indicate that it is in a “pending” state. Keep an eye on the channel log as you proceed with testing your function.

Back in the Google Cloud interface for your deployed function, you’ll see a set of tabs, one of which is labelled “Testing”. Enter the following JSON code into the field labelled “Triggering event”, then click the blue “Test the function” button:

{
  "customerId": "test",
  "message": "I'd like a pepperoni pizza, please"
}

If everything worked, you should see output in your Ably dev console indicating a successful invocation of the function.


Define function app details


Define function app details

Step 5. Configure a webhook

The next thing is to set up a webhook in the Ably dashboard, which we need in order to relay channel messages from the customer to our Google function for processing. We’ll actually be using two channels – one outbound, and one inbound. We’ll do this to ensure that only the customer’s communications are sent to Google Cloud, and so that the browser app need only process messages from the chat bot. Channels only consume resources while in use, so using two channels in this way is not a problem.

Just as in the previous step, go to the dashboard for the Ably app you’re using, choose the “Integrations” tab, then click “New Integration Rule”:


Create a new Integration rule

Choose the “Webhook” option:


Select the Webhook option

Choose the “Google Functions” option:


Select the Google Cloud option

Fill out the form to provide Ably with the details it needs to invoke your Google function.

  1. Select the Google Cloud region in which your project has been deployed
  2. Enter the project ID provided during project creation in step 3 of the tutorial. In the screenshot it was “ably-pizza-tutorial”, but you will have entered something of your own.
  3. Enter invokePizzaAssistant – the name of your Google function.
  4. Enter a regular expression that the webhooks will use to whitelist the Ably channels for which the event should be triggered. ^pizza:customer: means that the channel name must begin with the string pizza:customer:, with no restrictions on the characters that follow.
  5. Ensure that the “Enveloped” checkbox is unchecked, so that only the raw JSON from the app gets sent to Google Cloud.
  6. Click the blue “Create” button.


Fill out the form

Your webhook will now be created, and you’ll be able to continue developing your app.

Step 5: Developing the browser app

The browser app will be a simple text-based conversation with the Google Cloud-hosted chat bot. We’ll have a list of messages published by the customer and the chat bot, followed by a text box in which the user can type their responses to the chat bot’s messages. We’ll also include an initial message that serves as the default greeting that the pizza store will give to each new customer.

In the repository for this tutorial, in addition to the completed code further below, you’ll find the CSS to go with the code below, and a subtle background image to complement the app and give it some visual polish.

Note that all of the code below can be found in this tutorial’s repository, if you just want to skip to the end and see the application running.

(a) Getting the application skeleton in place

  1. Create a new project directory on your hard drive
  2. Create a blank file named app.js in your project directory. We’ll edit it later.
  3. Either create a blank file named app.css in your project directory, or copy the pre-made file of the same name from the repository.
  4. Create an HTML file named index.html for the user interface. Start with the code below:
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Ably's Pizza</title>

  <script src="https://cdn.ably.com/lib/ably.min-1.js"></script>

  <link rel="stylesheet" href="app.css">
</head>
<body>
  <div id="chat-interface">
    <h1>Ably's Realtime Pizza</h1>

    <div id="chat-log">
      <div class="bot message">
        Welcome to Ably's Realtime Pizza - the most responsive pizza this side
        of the sun! How may we assist you today?
      </div>
    </div>

    <div id="user-input">
      <input id="input-field" type="text" placeholder="Type your message here...">
    </div>
  </div>

  <!-- We include our application script at the end, to ensure the HTML above is
       already loaded and ready to interface with before the script runs: -->
  <script src="app.js"></script>
</body>
</html>

(b) Getting a basic application script working

The application script has a few parts to it, but isn’t too complex. The script will need to update the user interface at various points, and because the layout is fairly simple, we’re just going to use vanilla JavaScript code to accomplish this, rather than over-complicating things with frameworks such as Angular or React. For anything more ambitious, you’ll likely want to upgrade the code in this tutorial to use a framework of your choosing.

The first thing to do is to instantiate the Ably client library (already linked for you in the above HTML) and verify that we can send and receive messages successfully. The following code will define a channel, subscribe to it, and use it to publish customer messages. If everything is working, the fact that we’re subscribed to the same channel that we’re publishing to means that our messages will simply be echoed back to us via the receiveMessage handler function, which will then append them to the chat log in the HTML.

The code below should go in your app.js file.

Don’t forget to replace YOUR_API_KEY_GOES_HERE with your actual API key!

// This Ably client library is available due to the script in the HTML header:
const ably = new Ably.Realtime('YOUR_API_KEY_GOES_HERE');

// Remember when creating your API key that you specified that it would only
// work for channels starting with the string "pizza:". When testing the script,
// try changing the channel name to something else and see how Ably emits an
// error to your browser developer console.
const channel = ably.channels.get('pizza:messages');
channel.subscribe(receiveMessage);

// Define the listener function used above, and have it add the received message
// to the chat log in the user interface:
function receiveMessage(message) {
  // The appendMessageElement function is defined further below.
  appendMessageElement('bot', message.data.message);
}

// Retrieve references to the message input field and the chat log container:
const inputField = document.getElementById('input-field');
const chatLog = document.getElementById('chat-log');

// Define an event listener function to be triggered when a key is pressed:
function processInput(e) {
  if (e.which !== 13) return; // If it's not the ENTER key, don't do anything

  const message = inputField.value.trim(); // Grab the trimmed input message
  if (message.length > 0) { // Don't post blank messages

    // Asynchronously post the message to the Ably channel. The channel
    // subscription we created above will echo the message shortly:
    channel.publish('user', { message });
  }

  // Clear the input field, ready for the next message:
  inputField.value = '';
}

// Attach the above listener function to the message input field:
inputField.addEventListener('keydown', processInput);

// And finally, this function will append messages to the chat log. The first
// argument indicates whether the message comes from the user or the bot:
function appendMessageElement(type, message) {
  const div = document.createElement('div');

  // Add two CSS classes, because later we'll want to be able to visually
  // differentiate user messages from bot messages:
  div.classList.add(type, 'message');
  div.textContent = message;
  chatLog.appendChild(div);

  // If the conversation goes on for a while, messages will fall off the bottom
  // of the page. Instead, let's scroll to the bottom of the page automatically:
  const el = document.scrollingElement;
  el.scrollTop = el.scrollHeight;
}

With the above in place, fire up your browser and give it a go. Enter chat messages into the input box, press ENTER, and if all is well, they should be echoed back to you in less than a second. Try submitting a few messages one after the other.

If you’re not seeing your messages in the window, open your browser’s developer tools and check the console to see if there are any error messages being emitted by your script.

(c) Filling out the primary functionality

Now that you’ve verified that you’re able to connect to Ably and send and receive messages, it’s time to update the code to integrate it with everything you did earlier in this tutorial.

First, we’ll need two channels rather than one. The first channel is the outbound channel, and is where the webhook will be listening for messages to forward on to your Google function. The second channel is the inbound channel where the bot’s response messages will be posted, and is the channel to which your script will attach a listener.

If two different customers are trying to order a pizza at the same time, we don’t want their messages getting mixed in together. To address this, we’ll be appending a random customer identification value to the inbound and outbound channel names, with the effect being that every customer is allocated their own unique pair of inbound and outbound channels. The customer ID will also need to be included in outbound messages so that your Google function can figure out where to send responses. The channels will only be used for the duration of the chat session, which means the maximum number of active channels at any one time will be equal to twice the number of customers currently placing pizza orders. See the pricing page for details about the peak connection limit available for your Ably account.

Here’s some code to generate a random customer ID. This will be run when the page first loads, and you’ll be able to then use the customerID in your channel names:

// Generate a customer id, being sure to sandbox the setup code inside a closure
const customerId = (function () {

  // The full set of characters that can be included in a customer ID
  const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';

  // An arrow function to a random character from `chars`
  const randomChar = () => chars[Math.floor(Math.random() * chars.length)];

  // Change this if you want a shorter or longer ID
  const idLength = 16;

  // Prepare an array of our chosen customer ID size
  const id = new Array(idLength);

  // Populate the array with random characters
  for (let i = 0; i < idLength; i++) {
    id[i] = randomChar();
  }

  // Join the array entries together to form the customer ID string
  return id.join('');
})();

Split your channel initialization code to generate two channels instead of one:

// The customer's messages will be posted here
const outboundChannel = ably.channels.get('pizza:customer:' + customerId);

// Messages from the bot will be posted here, where we'll be listening for responses
const inboundChannel = ably.channels.get('pizza:bot:' + customerId);

// Update your subscription to listen to the inbound channel
inboundChannel.subscribe(receiveMessage);

We’re not using any special server-side persistence features, so we’ll need to keep track of the conversation context and order details ourselves, and pass them to the chat bot as the conversation progresses. Only the bot will be updating the context; we’ll just keep track of it on the bot’s behalf, so that it isn’t lost from one message to the next.

let context = null; // We start with an uninitialized conversation context

function receiveMessage(message) {
  context = message.data.context; // The bot returns an updated context
  appendMessageElement('bot', message.data.message);
}

Let’s refactor the message publishing code into its own function. We’ll also modify what we’re sending so that it includes the customer ID in the message data. This will make it easy for the Google function to reply on the correct channel without having to extract the customer ID from the name of the inbound channel:

function postMessage(message) {
  // Note the additional call to appendMessageElement, which previously was only
  // being used by the receiveMessage handler. Because we want to visually
  // distinguish user messages from bot messages, the first argument is 'user'
  // for outbound messages, and 'bot' for inbound messages. These are simply the
  // names of the CSS classes to be attached to the message div element appended
  // to the DOM.
  appendMessageElement('user', inputField.value.trim());

  // Post to the outbound channel, and wrap the customer ID, message and context
  // in a plain object that the webhook will include when forwarding the data
  // on to your Google function.
  outboundChannel.publish('user', { customerId, message, context });
}

Don’t forget to also update your processInput function to use postMessage rather than publishing directly:

function processInput(e) {
  if (e.which !== 13) { // Character code 13 is the ENTER key
    return;
  }

  const message = inputField.value.trim();
  if (message.length > 0) {
    postMessage(message);
  }

  inputField.value = '';
}

Once you’ve made the above changes to your application script, it’s time to test the app and have a conversation with your bot!

(d) Polishing up the experience

You may have noticed a short delay between your message submission and the bot’s response. It’d be good if we could get some visual feedback that they’re going to respond, so we’ll add a brief message that pretends they’re another person typing a response.

Also, the UI is not very useful unless the text input box is focused at all times, so we’ll add some additional event listeners to make sure that no matter where the user clicks on the page, or whether they leave or return to the browser window, the input box always automatically refocuses itself.

Modify the appendMessage function to return a reference to the HTML element it has just appended to the DOM:

function appendMessageElement(type, message) {
  const div = document.createElement('div');
  div.classList.add(type, 'message');
  div.textContent = message;
  chatLog.appendChild(div);

  const el = document.scrollingElement;
  el.scrollTop = el.scrollHeight;

  return div; // Add this line
}

Next, we’ll define a special “waiting” message that we add when the user types a message, and remove when a reply is received:

let waitingElement; // persist a reference to the HTML element

function setWaiting(isWaiting) { // isWaiting should be true or false
  if (isWaiting) {
    // If the waiting element already exists, remove it and re-add it so that it
    // is always the last message in the chat log:
    if (waitingElement) {
      waitingElement.remove();
    }
    // Note the "waiting" class which, if you're using the CSS provided in the
    // repository for this tutorial, will style the message so that it pulses
    // to indicate that something is happening in the background:
    waitingElement = appendMessageElement('waiting', 'typing ...');
  }
  else {
    // We'll call setWaiting(false) when the bot's response message is received,
    // which will trigger the following code to remove the waiting element:
    if (waitingElement) {
      waitingElement.remove();
      waitingElement = null;
    }
  }
}

Update the code to call the above function when sending and receiving messages:

function receiveMessage(message) {
  setWaiting(false); // The bot has replied, so remove the waiting message
  context = message.data.context;
  appendMessageElement('bot', message.data.message);
}

function postMessage(message) {
  appendMessageElement('user', inputField.value.trim());
  setWaiting(true); // Add a temporary waiting message
  outboundChannel.publish('user', { user: customerId, message });
}

Finally, at the end of the script where your text box event listener is defined, hook up a couple of extra event listeners to keep the user focused on the input text box:

function focusInputField() {
  inputField.focus();
}

// Re-focus the input field when the window regains focus:
window.addEventListener('focus', focusInputField);

// ... and if the user clicks anywhere else on the page:
document.body.addEventListener('click', focusInputField);

// When the page loads for the first time, focus the input field by default:
focusInputField();

Congratulations, you’re done!


Animated image of the browser app in action

Next Steps

1. Take a look at the Webhook documentation for further details about what was described in this tutorial.
2. If you would like to find out more about Ably features and capabilities, see Ably Integrations.
3. Learn more about Ably features by stepping through our other Ably tutorials
4. Gain a good technical overview of how the Ably realtime platform works
5. Get in touch if you need help