Build a WhatsApp Events Reminder App
Published on August 29, 2024

Introduction

In this tutorial, we will learn to set an Events Reminder WhatsApp conversation that allows you to receive daily messages containing today’s events to remember and query future events. We will set up a WhatsApp Sandbox for inbound and outbound messages and retrieve events from a Firebase Firestore database instance.

You can find the code for this project on GitHub.

Prerequisites

Project outline

By the end of this project, this is what your project folder should look like:

[node_modules]
.env
index.js
package-lock.json
package.json
events-reminder-store-credentials.json

Create a Node Project and Add the Dependencies

In your newly created project directory, initialize a new node project. This will create a new package.json file. The -y flag automatically fills in the defaults without asking for the details.

npm init -y

And install the following dependencies for our project. Alternatively, you can clone the project from GitHub and install the dependencies.

npm install axios dotenv express node-cron

These dependencies are important in our project to make and handle HTTP requests to external services, manage environment variables, set up our server, and schedule the daily messages containing the events.

Create a WhatsApp Sandbox

We will use the WhatsApp Sandbox for this tutorial, but you can also integrate your business WhatsApp account.

Using the Sandbox

The steps to use the Messages API Sandbox to send test messages on supported messaging platforms are as follows:

  1. Setup your sandbox

  2. Configure webhooks

Environment Variables

Create a .env file for your project and add the environment variables found in the code snippet below.

VONAGE_API_KEY=
VONAGE_API_SECRET=
VONAGE_WHATSAPP_NUMBER=
TO_NUMBER=

Michael's blog post provides a wonderful explanation of using environment variables in Node.js.

The VONAGE_API_KEY and VONAGE_API_SECRET can be found on the Vonage Dashboard.

It shows where the API Key and API Secrets are. Right under the text API key and API Secret]Vonage DashboardThe VONAGE_WHATSAPP_NUMBER can be found at the bottom of the Messages API Sandbox page.

TO_NUMBER is the number from which you're going to message the WhatsApp sandbox.

NOTE: Don't use a leading + or 00 when entering a phone number, start with the country code, for instance 16600800000.

Send a WhatsApp Message 

Once we open the Messages API Sandbox Page, a cURL command shows you how to add an endpoint and then message a WhatsApp number at the bottom of the page. I’ve taken that code and turned it into Node.js.

//index.js

require("dotenv").config();

const user = process.env.VONAGE_API_KEY;

const password = process.env.VONAGE_API_SECRET;

const from_number = process.env.VONAGE_WHATSAPP_NUMBER;

const to_number = process.env.VONAGE_TO_NUMBER;

const data = JSON.stringify({

  from: { type: "whatsapp", number: from_number },

  to: { type: "whatsapp", number: to_number },

  message: {

    content: {

      type: "text",

      text: "The events we have today are: event",

    },

  },

});

const https = require("https");

const options = {

  hostname: "messages-sandbox.nexmo.com",

  port: 443,

  path: "/v0.1/messages",

  method: "POST",

  authorization: {

    username: user,

    password: password,

  },

  headers: {

    Authorization: "Basic " + btoa(`${user}:${password}`),

    "Content-Type": "application/json",

  },

};

const req = https.request(options, (res) => {

  console.log(`statusCode: ${res.statusCode}`);

  res.on("data", (d) => {

    process.stdout.write(d);

  });

});

req.on("error", (e) => {

  console.error(e);

});

req.write(data);

req.end();

In this basic form, every time I run the file I’ve created, I get a message sent back with what’s contained in the variable text. This blog post shows an example of an inbound use of the WhatsApp sandbox. But because we want our events to remind us, we will use outbound for this tutorial’s example.

Generate a localtunnel.me URL using the command code: lt --port 8000, add the generated URL, and append /inbound and /event, respectively, and click 'Save webhooks'.

Now we switch; the WhatsApp sandbox responds once the user sends a message. Let's respond, "The events we have today are: events," once the user sends a message. But it's not picking any events yet; we simply send that text string. So, up to here, you've seen how to receive a WhatsApp once we run the file and how to receive an outbound response. Let's go to the next step, where we will actually get events from a database.

//index.js

require("dotenv").config();

const express = require("express");

const https = require("https");

const app = express();

app.use(express.json());

const PORT = 8000;

const from_number = process.env.VONAGE_WHATSAPP_NUMBER; // Your Vonage WhatsApp number

app.post("/inbound", (req, res) => {

  console.log("Inbound message received:", req.body);

  // Assuming the structure of req.body is as expected

  const to_number = req.body.from.number; // Sender's number to which you're replying

  console.log({to_number});

  const data = JSON.stringify({

    from: { type: "whatsapp", number: from_number },

    to: { type: "whatsapp", number: process.env.TO_NUMBER },

    message: {

      content: {

        type: "text",

        text: "The events we have today are: events",

      },

    },

  });

  const options = {

    hostname: "messages-sandbox.nexmo.com",

    port: 443,

    path: "/v0.1/messages",

    method: "POST",

    headers: {

      Authorization:

        "Basic " +

        Buffer.from(

          ${process.env.VONAGE_API_KEY}:${process.env.VONAGE_API_SECRET}

        ).toString("base64"),

      "Content-Type": "application/json",

      "Content-Length": Buffer.byteLength(data),

    },

  };

  const reqOut = https.request(options, (resOut) => {

    console.log(`statusCode: ${resOut.statusCode}`);

    resOut.on("data", (d) => {

      process.stdout.write(d);

    });

  });

  reqOut.on("error", (e) => {

    console.error(e);

  });

  reqOut.write(data);

  reqOut.end();

  res.status(200).send("Inbound message processed");

});

app.listen(PORT, () => {

  console.log(`Server is listening on port ${PORT}`);

});

Firebase

As stated at the beginning of this tutorial, you’ll need a Firebase account. Login to your Firebase account, create a Firebase project, set location, and create a billing account.

Firestore

Create a new Firestore database instance. You'll also need to create a service account and add the JSON file to your node project.

You need to create a collection. Populate the Firestore fields with the events. Create each event node, which should contain three fields: date, of type timestamp; details; and event_type, both of type string.

Add the code 

This code block shows how to import the Firebase Admin SDK, Initialize the Firebase app, load your service account from a local JSON file, and access your Firestore database.

const admin = require("firebase-admin");

admin.initializeApp({

  credential: admin.credential.cert(

    require("./events-reminder-store-credentials.json")

  ),

});

const db = admin.firestore();

Create the Function that Gets Today's Events

This function initializes a date range for the current day, starting from midnight to just before midnight on the next day. It then queries Firestore for documents in the events collection where the date field falls within this range. If documents are found, a string summarizing the day's events is constructed.

async function getEventsForToday() {

    const today = new Date();

    today.setHours(0, 0, 0, 0); // Set the time to the start of the day (midnight)

    const tomorrow = new Date(today);

    tomorrow.setDate(today.getDate() + 1); // Set to the start of the next day (midnight next day)

    try {

        // Query Firestore for events within today's date range

        const snapshot = await db.collection('events')

            .where('date', '>=', admin.firestore.Timestamp.fromDate(today))

            .where('date', '<', admin.firestore.Timestamp.fromDate(tomorrow))

            .get();

        if (snapshot.empty) {

            return 'No events found for today.'; // Return message if no events are found

        }

        let eventsText = 'Today\'s events: ';

        snapshot.forEach(doc => {

            const event = doc.data();

            eventsText += ${event.details}; ; // Concatenate each event detail into a string

        });

        return eventsText; // Return the string of today's events

    } catch (error) {

        console.error("Error fetching events:", error);

        return "Failed to fetch events."; // Handle errors in fetching data

    }

}

The same getEventsForToday function handles checking for today's events as described above. It sets the date to cover all events occurring from the start to the end of the current day, effectively fetching events scheduled for today.

async function handleDateRequest(dateString, requesterNumber) {

    const queryDate = new Date(dateString + 'T00:00:00Z'); // Set the query date start

    const queryDateEnd = new Date(queryDate);

    queryDateEnd.setDate(queryDate.getDate() + 1); // Set the query date end to the next day

    try {

        const snapshot = await db.collection('events')

            .where('date', '>=', admin.firestore.Timestamp.fromDate(queryDate))

            .where('date', '<', admin.firestore.Timestamp.fromDate(queryDateEnd))

            .get();

        if (snapshot.empty) {

            sendMessage('No events found for ' + dateString, requesterNumber); // Inform no events found

            return;

        }

        let eventsText = Events on ${dateString}: ;

        snapshot.forEach(doc => {

            const event = doc.data();

            eventsText += ${event.details}; ; // Aggregate event details

        });

        sendMessage(eventsText, requesterNumber); // Send the aggregated event details

    } catch (error) {

        console.error("Error retrieving events:", error);

    }

}

The code uses admin.firestore.Timestamp.fromDate() to convert JavaScript Date objects into Firestore-compatible timestamp formats. This is essential for accurately querying date fields stored in Firestore.

All Firestore interactions include try-catch blocks to handle and log errors effectively, ensuring that any issues during database operations are caught and can be diagnosed.

Schedule Future Messages

We could schedule a cloud function and deploy it with cloud functions. For this tutorial, though, we will use node-cron. I've set it to send messages daily at 09:50 a.m. in the "Europe/London" timezone, but you can adjust it to a time and timezone that makes more sense to you.

const cron = require("node-cron");

cron.schedule("50 09  *", async () => {

  console.log("Running a job at 09:50 at Europe/London timezone");

  const eventsText = await getEventsForToday();

  sendMessage(eventsText, TO_NUMBER);

}, {

  scheduled: true,

  timezone: "Europe/London",

});

Bonus: Add Tailored Messages Using Open AI

So many of us are hyped about using LLMs (large language models) these days. How about adding personalized messages to be sent out for these events you're being reminded about? You can make API calls to OpenAI to generate a personalized event message for you—you can then copy and paste it and use it!

Install the OpenAI npm dependency from your terminal.

npm install openai

Import the dependency to your JavaScript file.

const OpenAI = require("openai");

Generate an API key and set up the OpenAI client.

const openai = new OpenAI({ apiKey: OPENAI_API_KEY });

Add the OPENAI_API_KEY to your .env file.

OPENAI_API_KEY=

Create a generateCreativeMessage() function that uses OpenAI's API to generate customized text based on input event information. The function sends a request to the API, asking it to create text based on a prompt that includes event details. The function specifies a maximum word count for the response. If the request is successful, it trims and returns the generated text. If an error occurs, a fallback message still echoes the event details.

async function generateCreativeMessage(eventsText) {

  try {

    const response = await openai.completions.create({

      model: "gpt-3.5-turbo-instruct",

      prompt: Create a friendly and engaging message based on the following events: ${eventsText},

      max_tokens: 150,

    });

    return response.choices[0].text.trim();

  } catch (error) {

    console.error("Failed to generate message with OpenAI:", error);

    return Here's what's happening today: ${eventsText}; // Fallback text

  }

}

And let's not forget to update the scheduled message to add the generateCreativeMessage() function to create a tailored message based on the events. The original event details and the newly created message are sent to a specified phone number using the sendMessage() function.

cron.schedule(

  "50 09  *",

  async () => {

    console.log("Running a job at 09:50 at Europe/London timezone");

    const eventsText = await getEventsForToday();

    const creativeText = await generateCreativeMessage(eventsText);

    sendMessage(

      ${eventsText}\n\nSuggested message to send: ${creativeText},

      TO_NUMBER

    );

  },

  {

    scheduled: true,

    timezone: "Europe/London",

  }

);

Note: Consider API Costs, API Rate limits, and data privacy when using OpenAI's API.

Run The Application

The application is scheduled to check for today's events and send a message via WhatsApp at a specified time each day. You can query for events on specific dates by sending messages in the format "Events on YYYY-MM-DD?" through WhatsApp.

Run the lt command to start the tunnel.

lt --port 8000

Add the generated URL to your Messages API Sandbox Webhooks, and don't forget to click Save Webhooks.

Inbound field: Inbound HTTP Post. You add the generated url and add a /inbound at the end. Status field: HTTP POST. Inbound field: Inbound HTTP Post. You add the generated url and add a /status at the endMessages API WebhooksRun the JavaScript file you created on the same port the tunnel listens to.

node index.js

You can update the node.cron function to send a message at any given time, and you can also message the WhatsApp Sandbox number to ask for today's events in the format.

Possible Extension - Vonage AI Studio

We’ve achieved this using Vonage’s messages API, but you could also have used Vonage AI Studio to design the conversation and integrate your backend. It even has an add-on Gen AI node that lets you use the power of OpenAI’s Large Language Model to dynamize your virtual assistant to handle your user queries with the knowledge of context-specific nuance and the advantage of having the internet as its data source. Here's a tutorial for Building a FAQ Answering Service with OpenAI and Vonage AI Studio.

Conclusion

Today, you saw how to use the Vonage Messages API Sandbox integrated with Firebase Firestore and Cloud Functions for Firestore to receive event reminder messages. Join us on our Vonage Community Slack or send us a message on X, previously known as Twitter.

Amanda CavallaroDeveloper Advocate

Ready to start building?

Experience seamless connectivity, real-time messaging, and crystal-clear voice and video calls-all at your fingertips.

Subscribe to Our Developer Newsletter

Subscribe to our monthly newsletter to receive our latest updates on tutorials, releases, and events. No spam.