Send notifications for a web app using Cloud Messaging and Cloud Functions

1. Overview

In this codelab, you'll learn how to use Cloud Functions for Firebase to add functionality to a chat web app by sending notifications to users of the chat app.

3b1284f5144b54f6.png

What you'll learn

  • Create Google Cloud Functions using the Firebase SDK.
  • Trigger Cloud Functions based on Auth, Cloud Storage, and Cloud Firestore events.
  • Add Firebase Cloud Messaging support to your web app.

What you'll need

  • A credit card. Cloud Functions for Firebase requires the Firebase Blaze plan, which means you will have to enable billing on your Firebase project using a credit card.
  • The IDE/text editor of your choice such as WebStorm, Atom or Sublime.
  • A terminal to run shell commands with NodeJS v9 installed.
  • A browser such as Chrome.
  • The sample code. See next step for this.

2. Get the sample code

Clone the GitHub repository from the command line:

git clone https://github.com/firebase/friendlychat

Import the starter app

Using your IDE, open or import the android_studio_folder.pngcloud-functions-start directory from the sample code directory. This directory contains the starting code for the codelab which consists of a fully functional Chat Web App.

3. Create a Firebase project and Set up your app

Create project

In Firebase Console, click on Add Project and call it FriendlyChat.

Click Create Project.

Upgrade to the Blaze plan

To use Cloud Functions for Firebase and Cloud Storage for Firebase, your Firebase project needs to be on the pay-as-you go (Blaze) pricing plan, which means it's linked to a Cloud Billing account.

  • A Cloud Billing account requires a payment method, like a credit card.
  • If you're new to Firebase and Google Cloud, check if you're eligible for a $300 credit and a Free Trial Cloud Billing account.
  • If you're doing this codelab as part of an event, ask your organizer if there are any Cloud credits available.

If you do not have access to a credit card or are uncomfortable continuing with the Blaze pricing plan, consider using the Firebase Emulator Suite which will allow you to emulate Cloud Functions for free on your local machine.

All Firebase projects, including those on the Blaze pricing plan, still have access to no-cost usage quotas for Cloud Functions. The steps outlined in this codelab will fall within the free tier usage limits. However, you will see small charges (about $0.03) from Cloud Storage which is used to host your Cloud Functions build images.

To upgrade your project to the Blaze plan, follow these steps:

  1. In the Firebase console, select to upgrade your plan.
  2. Select the Blaze plan. Follow the on-screen instructions to link a Cloud Billing account to your project.
    If you needed to create a Cloud Billing account as part of this upgrade, you might need to navigate back to the upgrade flow in the Firebase console to complete the upgrade.

Enable Google Auth

To let users sign-in the app, we'll use Google auth which needs to be enabled.

In Firebase Console, open the Build section > Authentication > Sign-in method tab (or click here to go there). Then, enable the Google Sign-in Provider and click Save. This will allow users to sign in on the web app with their Google accounts.

Also, feel free to set the public facing name of your app to Friendly Chat:

8290061806aacb46.png

Set up Cloud Storage for Firebase

The app uses Cloud Storage to upload pictures.

Here's how to set up Cloud Storage for Firebase in your Firebase project:

  1. In the left-panel of the Firebase console, expand Build and then select Storage.
  2. Click Get started.
  3. Select a location for your default Storage bucket.
    Buckets in US-WEST1, US-CENTRAL1, and US-EAST1 can take advantage of the "Always Free" tier for Google Cloud Storage. Buckets in all other locations follow Google Cloud Storage pricing and usage.
  4. Click Start in test mode. Read the disclaimer about the security rules.
    Do not distribute or expose an app publicly without adding Security Rules for your Storage bucket.
  5. Click Create.

Add a Web App

On Firebase Console, add a web app. To do so, go to Project Settings and scroll down to Add app. Pick web as the platform and check the box for setting up Firebase Hosting, then register the app and click Next for the remainder of the steps, lastly clicking on Continue to console.

4. Install the Firebase Command Line Interface

The Firebase Command Line Interface (CLI) will allow you to serve the web app locally and deploy your web app and Cloud Functions.

To install or upgrade the CLI run the following npm command:

npm -g install firebase-tools

To verify that the CLI has been installed correctly, open a console and run:

firebase --version

Make sure the version of the Firebase CLI is above 4.0.0 so that it has all the latest features required for Cloud Functions. If not, run npm install -g firebase-tools to upgrade as shown above.

Authorize the Firebase CLI by running:

firebase login

Make sure you are in the cloud-functions-start directory, then set up the Firebase CLI to use your Firebase Project:

firebase use --add

Next, select your Project ID and follow the instructions. When prompted, you can choose any Alias, such as codelab.

5. Deploy and run the web app

Now that you have imported and configured your project, you are ready to run the web app for the first time! Open a terminal window, navigate to the cloud-functions-start folder, and deploy the web app to Firebase hosting using:

firebase deploy --except functions

This is the console output you should see:

i deploying database, storage, hosting
  database: rules ready to deploy.
i  storage: checking rules for compilation errors...
  storage: rules file compiled successfully
i  hosting: preparing ./ directory for upload...
  hosting: ./ folder uploaded successfully
 storage: rules file compiled successfully
 hosting: 8 files uploaded successfully
i starting release process (may take several minutes)...

 Deploy complete!

Project Console: https://console.firebase.google.com/project/friendlychat-1234/overview
Hosting URL: https://friendlychat-1234.firebaseapp.com

Open the web app

The last line should display the Hosting URL. The web app should now be served from this URL, which should be of the form https://<project-id>.firebaseapp.com. Open it. You should see a chat app's functioning UI.

Sign-in to the app by using the SIGN-IN WITH GOOGLE button and feel free to add some messages and post images:

3b1284f5144b54f6.png

If you sign-in the app for the first time on a new browser, make sure you allow notifications when prompted: 8b9d0c66dc36153d.png

We'll need to have notifications enabled at a later point.

If you have accidentally clicked Block, you can change this setting by clicking on the 🔒 Secure button on the left of the URL in the Chrome Omnibar and toggling the bar next to Notifications:

e926868b0546ed71.png

Now, we'll be adding some functionality using the Firebase SDK for Cloud Functions.

6. The Functions Directory

Cloud Functions allows you to easily have code that runs in the Cloud without having to setup a server. We'll be walking through how to build functions that react to Firebase Auth, Cloud Storage, and Firebase Firestore database events. Let's start with Auth.

When using the Firebase SDK for Cloud Functions, your Functions code will live under the functions directory (by default). Your Functions code is also a Node.js app and therefore needs a package.json that gives some information about your app and lists dependencies.

To make it easier for you, we've already created the functions/index.js file where your code will go. Feel free to inspect this file before moving forward.

cd functions
ls

If you are not familiar with Node.js, learning more about it before continuing the codelab would be helpful.

The package.json file already lists two required dependencies: the Firebase SDK for Cloud Functions and the Firebase Admin SDK. To install them locally, go to the functions folder and run:

npm install

Let's now have a look at the index.js file:

index.js

/**
 * Copyright 2017 Google Inc. All Rights Reserved.
 * ...
 */

// TODO(DEVELOPER): Import the Cloud Functions for Firebase and the Firebase Admin modules here.

// TODO(DEVELOPER): Write the addWelcomeMessage Function here.

// TODO(DEVELOPER): Write the blurImages Function here.

// TODO(DEVELOPER): Write the sendNotification Function here.

We'll import the required modules and then write three Functions in place of the TODOs. Let's start with importing the required Node modules.

7. Import the Cloud Functions and Firebase Admin modules

Two modules will be required during this codelab: firebase-functions enables writing Cloud Functions triggers and logs while firebase-admin enables using the Firebase platform on a server with admin access to do actions such as writing to Cloud Firestore or sending FCM notifications.

In the index.js file, replace the first TODO with the following:

index.js

/**
 * Copyright 2017 Google Inc. All Rights Reserved.
 * ...
 */

// Import the Firebase SDK for Google Cloud Functions.
const functions = require('firebase-functions');
// Import and initialize the Firebase Admin SDK.
const admin = require('firebase-admin');
admin.initializeApp();

// TODO(DEVELOPER): Write the addWelcomeMessage Function here.

// TODO(DEVELOPER): Write the blurImages Function here.

// TODO(DEVELOPER): Write the sendNotification Function here.

The Firebase Admin SDK can be configured automatically when deployed to a Cloud Functions environment or other Google Cloud Platform containers, and this happens when we call admin.initializeApp() with no arguments.

Now, let's add a Function that runs when a user signs in for the first time in the chat app, and we'll add a chat message to welcome the user.

8. Welcome New Users

Chat messages structure

Messages posted to the FriendlyChat chat feed are stored in Cloud Firestore. Let's have a look at the data structure we use for a message. To do this, post a new message to the chat that reads "Hello World":

11f5a676fbb1a69a.png

This should appear as:

fe6d1c020d0744cf.png

In Firebase Console, click on Firestore Database under the Build section. You should see the messages collection and one document containing the message that you wrote:

442c9c10b5e2b245.png

As you can see, chat messages are stored in Cloud Firestore as a document with name, profilePicUrl, text, and timestamp attributes added to the messages collection.

Adding welcome messages

The first Cloud Function adds a message that welcomes new users to the chat. For this, we can use the trigger functions.auth().onCreate, which runs the function every time a user signs-in for the first time in the Firebase app. Add the addWelcomeMessages function into your index.js file:

index.js

// Adds a message that welcomes new users into the chat.
exports.addWelcomeMessages = functions.auth.user().onCreate(async (user) => {
  functions.logger.log('A new user signed in for the first time.');
  const fullName = user.displayName || 'Anonymous';

  // Saves the new welcome message into the database
  // which then displays it in the FriendlyChat clients.
  await admin.firestore().collection('messages').add({
    name: 'Firebase Bot',
    profilePicUrl: '/images/firebase-logo.png', // Firebase logo
    text: `${fullName} signed in for the first time! Welcome!`,
    timestamp: admin.firestore.FieldValue.serverTimestamp(),
  });
  functions.logger.log('Welcome message written to database.');
});

Adding this function to the special exports object is Node's way of making the function accessible outside of the current file and is required for Cloud Functions.

In the function above, we are adding a new welcome message posted by "Firebase Bot" to the list of chat messages. We are doing this by using the add method on the messages collection in Cloud Firestore, which is where the messages of the chat are stored.

Since this is an asynchronous operation, we need to return the Promise indicating when Cloud Firestore has finished writing so the Cloud Functions don't execute too early.

Deploy Cloud Functions

Cloud Functions will only be active after you've deployed them. To do so, run this on the command line:

firebase deploy --only functions

This is the console output you should see:

i  deploying functions
i  functions: ensuring necessary APIs are enabled...
  functions: missing necessary APIs. Enabling now...
i  env: ensuring necessary APIs are enabled...
  env: missing necessary APIs. Enabling now...
i  functions: waiting for APIs to activate...
i  env: waiting for APIs to activate...
  env: all necessary APIs are enabled
  functions: all necessary APIs are enabled
i  functions: preparing functions directory for uploading...
i  functions: packaged functions (X.XX KB) for uploading
  functions: functions folder uploaded successfully
i  starting release process (may take several minutes)...
i  functions: creating function addWelcomeMessages...
  functions[addWelcomeMessages]: Successful create operation. 
  functions: all functions deployed successfully!

  Deploy complete!

Project Console: https://console.firebase.google.com/project/friendlypchat-1234/overview

Test the function

Once the function has deployed successfully, you'll need to have a user that signs in for the first time.

  1. Open your app in your browser using the hosting URL (in the form of https://<project-id>.firebaseapp.com).
  2. With a new user, sign in for the first time in your app using the Sign In button.
  • If you have already signed into the, app you can open Firebase Console Authentication and delete your account from the list of users. Then, sign in again.

262535d1b1223c65.png

  1. After you sign in, a welcome message should be displayed automatically:

1c70e0d64b23525b.png

9. Images moderation

Users can upload all type of images in the chat, and it is always important to moderate offensive images, especially in public social platforms. In FriendlyChat, the images that are being published to the chat are stored into Cloud Storage buckets.

With Cloud Functions, you can detect new image uploads using the functions.storage().onFinalize trigger. This will run every time a new file is uploaded or modified in Cloud Storage.

To moderate images, we'll go through the following process:

  1. Check if the image is flagged as Adult or Violent using the Cloud Vision API.
  2. If the image has been flagged, download it on the running Functions instance.
  3. Blur the image using ImageMagick.
  4. Upload the blurred image to Cloud Storage.

Enable the Cloud Vision API

Since we'll be using the Google Cloud Vision API in this function, you must enable the API on your firebase project. Follow this link, then select your Firebase project and enable the API:

5c77fee51ec5de49.png

Install dependencies

To moderate images, we'll use the Google Cloud Vision Client Library for Node.js, @google-cloud/vision, to run images through the Cloud Vision API to detect inappropriate images.

To install this package into your Cloud Functions app, run the following npm install --save command. Make sure that you do this from the functions directory.

npm install --save @google-cloud/vision@2.4.0

This will install the package locally and add them as a declared dependency in your package.json file.

Import and configure dependencies

To import the dependencies that were installed and some Node.js core modules (path, os and fs) that we'll need in this section, add the following lines to the top of your index.js file:

index.js

const Vision = require('@google-cloud/vision');
const vision = new Vision.ImageAnnotatorClient();
const {promisify} = require('util');
const exec = promisify(require('child_process').exec);

const path = require('path');
const os = require('os');
const fs = require('fs');

Since your function will run inside a Google Cloud environment, there is no need to configure the Cloud Storage and Cloud Vision libraries: they will be configured automatically to use your project.

Detecting inappropriate images

You'll be using the functions.storage.onChange Cloud Functions trigger, which runs your code as soon as a file or folder is created or modified in a Cloud Storage bucket. Add the blurOffensiveImages Function into your index.js file:

index.js

// Checks if uploaded images are flagged as Adult or Violence and if so blurs them.
exports.blurOffensiveImages = functions.runWith({memory: '2GB'}).storage.object().onFinalize(
    async (object) => {
      const imageUri = `gs://${object.bucket}/${object.name}`;
      // Check the image content using the Cloud Vision API.
      const batchAnnotateImagesResponse = await vision.safeSearchDetection(imageUri);
      const safeSearchResult = batchAnnotateImagesResponse[0].safeSearchAnnotation;
      const Likelihood = Vision.protos.google.cloud.vision.v1.Likelihood;
      if (Likelihood[safeSearchResult.adult] >= Likelihood.LIKELY ||
          Likelihood[safeSearchResult.violence] >= Likelihood.LIKELY) {
        functions.logger.log('The image', object.name, 'has been detected as inappropriate.');
        return blurImage(object.name);
      }
      functions.logger.log('The image', object.name, 'has been detected as OK.');
    });

Note that we added some configuration of the Cloud Functions instance that will run the function. With .runWith({memory: '2GB'}), we're requesting that the instance gets 2GB of memory rather than the default, because this function is memory intensive.

When the function is triggered, the image is ran through the Cloud Vision API to detect if it is flagged as adult or violent. If the image is detected as inappropriate based on these criteria, we're blurring the image, which is done in the blurImage function as we'll see next.

Blurring the image

Add the following blurImage function in your index.js file:

index.js

// Blurs the given image located in the given bucket using ImageMagick.
async function blurImage(filePath) {
  const tempLocalFile = path.join(os.tmpdir(), path.basename(filePath));
  const messageId = filePath.split(path.sep)[1];
  const bucket = admin.storage().bucket();

  // Download file from bucket.
  await bucket.file(filePath).download({destination: tempLocalFile});
  functions.logger.log('Image has been downloaded to', tempLocalFile);
  // Blur the image using ImageMagick.
  await exec(`convert "${tempLocalFile}" -channel RGBA -blur 0x24 "${tempLocalFile}"`);
  functions.logger.log('Image has been blurred');
  // Uploading the Blurred image back into the bucket.
  await bucket.upload(tempLocalFile, {destination: filePath});
  functions.logger.log('Blurred image has been uploaded to', filePath);
  // Deleting the local file to free up disk space.
  fs.unlinkSync(tempLocalFile);
  functions.logger.log('Deleted local file.');
  // Indicate that the message has been moderated.
  await admin.firestore().collection('messages').doc(messageId).update({moderated: true});
  functions.logger.log('Marked the image as moderated in the database.');
}

In the above function, the image binary is downloaded from Cloud Storage. The image is then blurred using ImageMagick's convert tool, and the blurred version is re-uploaded on the Storage Bucket. Next, we delete the file on the Cloud Functions instance to free up some disk space, and we do this because the same Cloud Functions instance can get re-used and if files are not cleaned up, it could run out of disk space. Finally, we add a boolean to the chat message indicating the image was moderated, and this will trigger a refresh of the message on the client.

Deploy the Function

The Function will only be active after you've deployed it. On the command line, run firebase deploy --only functions:

firebase deploy --only functions

This is the console output you should see:

i  deploying functions
i  functions: ensuring necessary APIs are enabled...
  functions: all necessary APIs are enabled
i  functions: preparing functions directory for uploading...
i  functions: packaged functions (X.XX KB) for uploading
  functions: functions folder uploaded successfully
i  starting release process (may take several minutes)...
i  functions: updating function addWelcomeMessages...
i  functions: creating function blurOffensiveImages...
  functions[addWelcomeMessages]: Successful update operation.
  functions[blurOffensiveImages]: Successful create operation.
  functions: all functions deployed successfully!

  Deploy complete!

Project Console: https://console.firebase.google.com/project/friendlychat-1234/overview

Test the function

Once the function has deployed successfully:

  1. Open your app in your browser using the hosting URL (in the form of https://<project-id>.firebaseapp.com).
  2. Once signed into the app, upload an image: 4db9fdab56703e4a.png
  3. Choose your best offensive image to upload (or you can use this flesh eating Zombie!) and after a few moments, you should see your post refresh with a blurred version of the image: 83dd904fbaf97d2b.png

10. New Message Notifications

In this section, you will add a Cloud Function that sends notifications to participants of the chat when a new message is posted.

Using Firebase Cloud Messaging (FCM), you can reliably send notifications to users across platforms. To send a notification to a user, you need their FCM device token. The chat web app that we are using already collects device tokens from users when they open the app for the first time on a new browser or device. These tokens are stored in Cloud Firestore in the fcmTokens collection.

If you would like to learn how to get FCM device tokens on a web app, you can go through the Firebase Web Codelab.

Send notifications

To detect when new messages are posted, you'll be using the functions.firestore.document().onCreate Cloud Functions trigger, which runs your code when a new object is created at a given path of Cloud Firestore. Add the sendNotifications function into your index.js file:

index.js

// Sends a notifications to all users when a new message is posted.
exports.sendNotifications = functions.firestore.document('messages/{messageId}').onCreate(
  async (snapshot) => {
    // Notification details.
    const text = snapshot.data().text;
    const payload = {
      notification: {
        title: `${snapshot.data().name} posted ${text ? 'a message' : 'an image'}`,
        body: text ? (text.length <= 100 ? text : text.substring(0, 97) + '...') : '',
        icon: snapshot.data().profilePicUrl || '/images/profile_placeholder.png',
        click_action: `https://${process.env.GCLOUD_PROJECT}.firebaseapp.com`,
      }
    };

    // Get the list of device tokens.
    const allTokens = await admin.firestore().collection('fcmTokens').get();
    const tokens = [];
    allTokens.forEach((tokenDoc) => {
      tokens.push(tokenDoc.id);
    });

    if (tokens.length > 0) {
      // Send notifications to all tokens.
      const response = await admin.messaging().sendToDevice(tokens, payload);
      await cleanupTokens(response, tokens);
      functions.logger.log('Notifications have been sent and tokens cleaned up.');
    }
  });

In the Function above, we are gathering all users' device tokens from the Cloud Firestore database and sending a notification to each of these using the admin.messaging().sendToDevice function.

Cleanup the tokens

Lastly, we want to remove the tokens that are no longer valid. This happens when the token that we once got from the user is not being used by the browser or device anymore. For instance, this happens if the user has revoked the notification permission for the browser session. To do this, add the following cleanupTokens function in your index.js file:

index.js

// Cleans up the tokens that are no longer valid.
function cleanupTokens(response, tokens) {
 // For each notification we check if there was an error.
 const tokensDelete = [];
 response.results.forEach((result, index) => {
   const error = result.error;
   if (error) {
     functions.logger.error('Failure sending notification to', tokens[index], error);
     // Cleanup the tokens that are not registered anymore.
     if (error.code === 'messaging/invalid-registration-token' ||
         error.code === 'messaging/registration-token-not-registered') {
       const deleteTask = admin.firestore().collection('fcmTokens').doc(tokens[index]).delete();
       tokensDelete.push(deleteTask);
     }
   }
 });
 return Promise.all(tokensDelete);
}

Deploy the Function

The function will only be active after you've deployed it, and to deploy it, run this in command line:

firebase deploy --only functions

This is the console output you should see:

i  deploying functions
i  functions: ensuring necessary APIs are enabled...
  functions: all necessary APIs are enabled
i  functions: preparing functions directory for uploading...
i  functions: packaged functions (X.XX KB) for uploading
  functions: functions folder uploaded successfully
i  starting release process (may take several minutes)...
i  functions: updating function addWelcomeMessages...
i  functions: updating function blurOffensiveImages...
i  functions: creating function sendNotifications...
  functions[addWelcomeMessages]: Successful update operation.
  functions[blurOffensiveImages]: Successful updating operation.
  functions[sendNotifications]: Successful create operation.
  functions: all functions deployed successfully!

  Deploy complete!

Project Console: https://console.firebase.google.com/project/friendlychat-1234/overview

Test the function

  1. Once the function has deployed successfully, open your app in your browser using the hosting URL (in the form of https://<project-id>.firebaseapp.com).
  2. If you sign-in the app for the first time, make sure you allow notifications when prompted: 8b9d0c66dc36153d.png
  3. Close the chat app tab or display a different tab: Notifications appear only if the app is in the background. If you would like to learn how to receive messages while your app is in the foreground, have a look at our documentation.
  4. Using a different browser (or an Incognito window), sign into the app and post a message. You should see a notification displayed by the first browser: 45282ab12b28b926.png

11. Congratulations!

You have used the Firebase SDK for Cloud Functions and added server-side components to a chat app.

What we've covered

  • Authoring Cloud Functions using the Firebase SDK for Cloud Functions.
  • Trigger Cloud Functions based on Auth, Cloud Storage, and Cloud Firestore events.
  • Add Firebase Cloud Messaging support to your web app.
  • Deploy Cloud Functions using the Firebase CLI.

Next Steps

Learn More