In this codelab, you'll learn how to use the Firebase SDK for Google Cloud Functions to improve an AngularFire Chat Web app and how to use Cloud Functions to send notifications to users of the Chat app.

What you'll learn

What you'll need

Clone the GitHub repository from the command line:

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

Import the starter app

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

Create project

In the Firebase console click on CREATE NEW PROJECT and call it FriendlyChat.

Initialize the web app

In the Firebase Console, in the Overview click the Add Firebase to your web app button.

The console will display an HTML/JavaScript snippet similar to this:

Copy the config values in the snippet and paste them into the src/environments/environment.prod.ts file where the TODO is located. This adds your Firebase config to the AngularFire Chat Web App. You can optionally add the values to src/environments/environment.ts as well for local development.

Enable service worker for Firebase Cloud Messaging

Copy the messagingSenderId value from the previous step (in src/environments/environment.prod.ts) to src/firebase-messaging-sw.js

Enable Google Auth

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

In the Authentication section > SIGN IN METHOD tab (click here to go there) you need to enable the Google Sign-in Provider and click SAVE. This will allow users to sign-in the Web app with their Google accounts

The Angular Command Line Interface (CLI) will allow you to build the web app locally before deploying it.

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

npm -g install @angular/cli

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

ng version

Make sure the @angular/cli version is at least 1.0.0 so that it has all the latest features required for Cloud Functions. If not, run npm install -g @angular/cli to upgrade as shown above.

The package.json file lists a number of dependencies required to build and run the AngularFire Chat Web App. To install them locally run npm install from the cloud-functions-angular-start folder:

npm install

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 Firebase version is at least 3.5.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-angular-start directory then set up the Firebase CLI to use your Firebase Project:

firebase use --add

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

Now that you have imported and configured your project you are ready to run the web app for the first time. Open a console at the cloud-functions-angular-start folder and run ng build to prepare the AngularFire Chat Web App for deployment:

ng build --prod --aot

This is the console output you should see:

Time: 14847ms
chunk    {0} polyfills.2d45a4c73c85e24fe474.bundle.js (polyfills) 158 kB {4} [initial] [rendered]
chunk    {1} main.6ec7a8ec4feb319ef346.bundle.js (main) 67 kB {3} [initial] [rendered]
chunk    {2} styles.104a2520eb3e11acebf6.bundle.css (styles) 69 bytes {4} [initial] [rendered]
chunk    {3} vendor.cf8a6fb34b438b442c98.bundle.js (vendor) 1.5 MB [initial] [rendered]
chunk    {4} inline.917facec1b2340079880.bundle.js (inline) 0 bytes [entry] [rendered]

Now run firebase deploy --except functions which will only deploy the web app to Firebase hosting:

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/YOUR_PROJECT_ID/overview
Hosting URL: https://YOUR_PROJECT_ID.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://YOUR_PROJECT_ID.firebaseapp.com. Open it. You should see a chat app's functioning UI.

If you are prompted for the "Show Notifications" permission, click Allow.

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

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

Cloud Functions allows you to easily have code that runs in the cloud without a server. We'll show you how to build functions that react to Firebase Auth, Cloud Storage and Firebase Realtime Database events. Let's start with Auth.

When using the Firebase SDK for Cloud Functions your code will live under the functions directory. 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 it will help to learn more about it before continuing the codelab.

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

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.

// (OPTIONAL) TODO(DEVELOPER): Write the annotateMessages Function here.

We'll first import the required modules and then write three Functions in place of the TODOs. First let's import the required Node modules.

Two modules will be required during this codelab, the firebase-functions module allows us to write the Cloud Functions trigger rules while the firebase-admin module allows us to use the Firebase platform on a server with admin access, for instance to write to the Realtime Database or send Firebase Cloud Messaging 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(functions.config().firebase);

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

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

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

// (OPTIONAL) TODO(DEVELOPER): Write the annotateMessages Function here.

The Firebase Admin SDK can be configured automatically when deployed on a Cloud Functions environment. This is what we do above using admin.initializeApp(functions.config().firebase);

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

The first Cloud Function adds a message that welcomes new users into 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 your 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((event) => {
  const user = event.data;
  console.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.
  return admin.database().ref('messages').push({
    name: 'Firebase Bot',
    photoUrl: '/assets/images/firebase-logo.png', // Firebase logo
    text: `${fullName} signed in for the first time! Welcome!`
  });
});

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 push method on the /messages collection in the Firebase Realtime Database which is where the messages of the chat are stored.

Since this is an asynchronous operation, we need to return the Promise indicating when the Realtime Database write has finished, so that Functions doesn't exit the execution too early.

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: 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/YOUR_PROJECT_ID/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://YOUR_PROJECT_ID.firebaseapp.com).
  2. With a new user, sign in for the first time in your app using the Sign In button.

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

Users can upload all type of images and it is always important to moderate offensive images especially in public social platforms. With Cloud Functions, you can detect new image uploads using the functions.storage().onChange 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 in 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, select your Firebase project and enable the API:

Note that you will have to enable billing to enable this API. While most of what you do in this codelab should be within the free tier there is a risk you may be billed depending on your usage. If you do not feel comfortable enabling Billing feel free to skip this section and move on to New Message Notifications.

Install dependencies

To moderate the images we'll need a few Node.js packages:

To install these three packages 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 @google-cloud/storage child-process-promise

This will install the three packages locally and add them as declared dependencies in your package.js file.

Import and configure dependencies

To import the three dependencies that were installed add the following lines to the top of your index.js file:

index.js

const gcs = require('@google-cloud/storage')();
const vision = require('@google-cloud/vision')();
const exec = require('child-process-promise').exec;

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

// Blurs uploaded images that are flagged as Adult or Violence.
exports.blurOffensiveImages = functions.storage.object().onChange((event) => {
  const object = event.data;
  // Exit if this is a deletion or a deploy event.
  if (object.resourceState === 'not_exists') {
    return console.log('This is a deletion event.');
  } else if (!object.name) {
    return console.log('This is a deploy event.');
  }

  const messageId = object.name.split('/')[1];
  const bucket = gcs.bucket(object.bucket);
  const file = bucket.file(object.name);

  return admin.database().ref(`/messages/${messageId}/moderated`).once('value')
    .then((snapshot) => {
      // The image has already been moderated.
      if (snapshot.val()) {
        return;
      }

      // Check the image content using the Cloud Vision API.
      return vision.detectSafeSearch(file);
    })
    .then((safeSearchResult) => {
      if (safeSearchResult[0].adult || safeSearchResult[0].violence) {
        console.log('The image', object.name, 'has been detected as inappropriate.');
        return blurImage(object.name, bucket);
      } else {
        console.log('The image', object.name,'has been detected as OK.');
      }
    });
});

In the Function above we only care about newly added images, so we just exit if the action was a delete. 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 which 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.
function blurImage(filePath, bucket, metadata) {
  const fileName = filePath.split('/').pop();
  const tempLocalFile = `/tmp/${fileName}`;
  const messageId = filePath.split('/')[1];

  // Download file from bucket.
  return bucket.file(filePath).download({ destination: tempLocalFile })
    .then(() => {
      console.log('Image has been downloaded to', tempLocalFile);
      // Blur the image using ImageMagick.
      return exec(`convert ${tempLocalFile} -channel RGBA -blur 0x24 ${tempLocalFile}`);
    })
    .then(() => {
      console.log('Image has been blurred');
      // Uploading the Blurred image back into the bucket.
      return bucket.upload(tempLocalFile, { destination: filePath });
    })
    .then(() => {
      console.log('Blurred image has been uploaded to', filePath);
      // Indicate that the message has been moderated.
      return admin.database().ref(`/messages/${messageId}`).update({ moderated: true });
    })
    .then(() => {
      console.log('Marked the image as moderated in the database.');
    });
}

In the above function the image binary is downloaded from Cloud Storage. Then the image is blurred using ImageMagick's convert tool and the blurred version is re-uploaded on the Storage Bucket. Finally we add a boolean to the chat message indicating the image was moderated, 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/YOUR_PROJECT_ID/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://YOUR_PROJECT_ID.firebaseapp.com).
  2. Download this offensive flesh eating Zombies image (hopefully it's not too offensive!) to your computer.
  3. Once signed-in the app upload the offensive zombie image:
  4. After short moment you should see your post refresh with a blurred version of the image:

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 send notifications to your users in a cross platform and reliable way. 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 the Firebase Realtime Database under the /fcmTokens/$deviceToken path.

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.database().path().onWrite Cloud Functions trigger which runs your code when a write to a given path of the Firebase Realtime Database is detected. 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.database.ref('/messages/{messageId}').onWrite((event) => {
  const snapshot = event.data;
  // Only send a notification when a message has been created.
  if (snapshot.previous.val()) {
    return;
  }

  // Notification details.
  const text = snapshot.val().text;
  const payload = {
    notification: {
      title: `${snapshot.val().name} posted ${text ? 'a message' : 'an image'}`,
      body: text ? (text.length <= 100 ? text : text.substring(0, 97) + '...') : '',
      icon: snapshot.val().photoUrl || '/assets/images/profile_placeholder.png',
      click_action: `https://${functions.config().firebase.authDomain}`
    }
  };

  // Get the list of device tokens.
  return admin.database().ref('fcmTokens').once('value').then(allTokens => {
    if (allTokens.val()) {
      // Listing all tokens.
      const tokens = Object.keys(allTokens.val());

      // Send notifications to all tokens.
      return admin.messaging().sendToDevice(tokens, payload).then(response => {
        // For each message check if there was an error.
        const tokensToRemove = [];
        response.results.forEach((result, index) => {
          const error = result.error;
          if (error) {
            console.error('Failure sending notification to', tokens[index], error);
            // Cleanup the tokens who are not registered anymore.
            if (error.code === 'messaging/invalid-registration-token' ||
                error.code === 'messaging/registration-token-not-registered') {
              tokensToRemove.push(allTokens.ref.child(tokens[index]).remove());
            }
          }
        });
        return Promise.all(tokensToRemove);
      });
    }
  });
});

In the Function above we are first exiting if the function was triggered because of an update in the message as we only want to send notifications for new messages. Then we're gathering all users' device tokens from the Firebase Realtime Database and sending a notification to each of these.

Lastly we're removing the tokens that are not valid anymore. This happens when the token that we once got from the user is not being used by the browser or device for instance if the user has revoked access.

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: 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/YOUR_PROJECT_ID/overview

Test the function

Once the function has deployed successfully you'll need to have a user that signs-in for the first time. If you have signed-in already with your account you can open the Firebase Console Authentication section and delete your account from the list of users.

  1. 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:
  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, sign into the app and post a message. You should see a notification displayed by the first browser:

Some chat rooms might want to react to the content of users' messages. With Cloud Functions, you can analyze a message's text content using the functions.database().path().onWrite trigger. This will run every time a new message is created or modified.

To annotate messages we'll go through the following process:

  1. Annotate new messages using the Cloud Natural Language API
  2. Update the messages with detected sentiment and entities.
  3. In the Angular app, change the font and boldness of new messages based on their sentiment.
  4. Also in the Angular app, display a list of recently discussed topics as new messages are added.

Enable the Cloud Natural Language API

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

Note that you will have to enable billing to enable this API. While most of what you do in this codelab should be within the free tier there is a risk you may be billed depending on your usage. If you do not feel comfortable enabling Billing feel free to skip this section.

Install dependencies

To moderate the images we'll need another Node.js package:

To install these three packages 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/language

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

Import and configure dependencies

To import the new dependency that was installed add the following line to the top of your index.js file:

index.js

const language = require('@google-cloud/language')();

Since your function will run inside a Google Cloud environment, there is no need to configure the Cloud Natural Language library: it will be configured automatically to use your project.

Detecting sentiment and entities

You'll be using the functions.database().path().onWrite Cloud Functions trigger which runs your code as soon as message is created or modified. Add the annotateMessages Function into your index.js file:

index.js

// Annotates messages using the Cloud Natural Language API
exports.annotateMessages = functions.database.ref('/messages/{messageId}').onWrite((event) => {
  const snapshot = event.data;
  const messageId = event.params.messageId;

  // Only annotate new messages.
  if (snapshot.previous.val() || !snapshot.val().text) {
    return;
  }

  // Annotation arguments.
  const text = snapshot.val().text;
  const options = {
    entities: true,
    sentiment: true
  };

  console.log('Annotating new message.');

  // Detect the sentiment and entities of the new message.
  return language.annotate(text, options)
    .then((result) => {
      console.log('Saving annotations.');

      // Update the message with the results.
      return admin.database().ref(`/messages/${messageId}`).update({
        sentiment: result[0].sentiment,
        entities: result[0].entities.map((entity) => {
          return {
            name: entity.name,
            salience: entity.salience
          };
        })
      });
    });
});

In the Function above we only care about newly created text-based messages, so we just exit if the action was an update or it was an image-based message. The message text is run through the Cloud Natural Language API to detect sentiment and entities. The message is then updated with annotation results, which 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 update operation.
✔  functions[sendNotifications]: Successful update operation.
✔  functions[annotateMessages]: Successful create operation.
✔  functions: all functions deployed successfully!

✔  Deploy complete!

Project Console: https://console.firebase.google.com/project/YOUR_PROJECT_ID/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://YOUR_PROJECT_ID.firebaseapp.com).
  2. Add some messages and observe how they are displayed based on their sentiment.
  3. Observe also the list of recently discussed topics that appears as you add new messages.

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

What we've covered

Next Steps

Learn More