How to Build a Football Tracker with Twilio WhatsApp and Node.js

September 20, 2023
Written by
Anderson Osayerie
Contributor
Opinions expressed by Twilio contributors are their own
Reviewed by

In today's digital age, messaging platforms have become an integral part of our daily lives. One platform that stands out is WhatsApp. WhatsApp as an integral part of our daily communication, helps to connect to friends, family, and customers. With the help of Twilio and Node.js, we can harness its power to create engaging and interactive applications.

In this article, I will guide you through the process of building a football WhatsApp bot using the Twilio API for WhatsApp, Rapid API, and Node.js. By the end, you'll have a robust bot that can provide live scores, match results, league standings, and more!

Prerequisites

  • Node.JS 16+. You can download an installer from Nodejs.org
  • ngrok. This is used to connect your Express application running locally to a public URL that Twilio can connect to from the Internet.
  • A smartphone with WhatsApp installed.
  • A Twilio account. If you are new to Twilio, you can create a free account here.
  • A Rapid API account.

Setting up the development environment

Creating application directory and installing dependencies

Launch your terminal and navigate to your desired location to create the project directory. Run the following commands to create a directory for your project and initialize an npm project inside it:

mkdir football-bot
cd football-bot
npm init -y

Execute the following commands to install the Express framework and other necessary dependencies needed for this project.

npm install express twilio nodemon dotenv axios dayjs

The other packages listed above would be needed for the following:

  • twilio - Twilio’s Node.js library used to handle sending messages.
  • nodemon - a package that monitors your project directory and automatically restarts your node application when it detects any changes.
  • dotenv - a package that is used to load the environment variables.
  • axios - a promise-based library for making HTTP requests.
  • dayjs - a package used to parse, manipulate, and display dates and times.

Using your favorite code editor, create an index.js file in the root folder of your application and add the following code:

const express = require("express");

const app = express();

const PORT = 5000;

app.use(express.json());
app.use(express.urlencoded({ extended: true }));

// start the server on port 5000
app.listen(PORT, () =>
  console.log(`App listening at http://localhost:${PORT}`)
);

To allow nodemon to watch your files and automatically reload the server when you make changes, add this line to the scripts section in the package.json file:

"dev": "nodemon index.js"

To start your local server on port 5000, run the following command in your terminal:

npm run dev

Setting up Rapid API

The source of data for the chatbot application is Zeus API on the Rapid Api platform. You would need to get an API key from Rapid API. Create a free account and log in to the account. Proceed to the Zeus Api page and subscribe to the API to be able to get access to test the API.

Zeus API page on Rapid API"s website

For this tutorial, you will be using the X-RapidAPI-Key located in the Code Snippets section in the image above. This token will need to be sent in the request header of every request to verify your identity. Ensure you only copy the key without the single quotes.

Create a .env file in the root directory of your project, open it, and paste the API key in this format:

RAPIDAPI_KEY=<YOUR_RAPIDAPI_KEY>

Create chatbot

Now that you have set up your development environment, it is time to build the chatbot. For this tutorial the chatbot will be very simple. It will look for particular keywords in the messages sent by the user and send back an appropriate response.

Send and receive message

This section will explain the process of implementing the functionality needed for receiving and responding to messages sent through WhatsApp.

Create a utils folder in the root directory of your project where you would add all the helper functions needed. Create a twilio.js file inside the folder and add the following code:

const MessagingResponse = require("twilio").twiml.MessagingResponse;

const sendResponse = (message) => {
  const twiml = new MessagingResponse();
  twiml.message(message);
  return twiml.toString();
};

module.exports = { sendResponse };

In the code snippet above, the sendResponse function makes use of TwiML (Twilio's Markup Language) to specify the desired response to incoming messages received by Twilio. The MessagingResponse helper class from Twilio's package is utilized to generate the TwiML.

To receive event notifications from Twilio, you must set up a route. The Twilio API for WhatsApp uses webhooks in order to interact with users. The webhook delivers data that includes the incoming message.

Update the index.js with the following code to import the sendResponse function and to create a route in the index.js file. This route handler is triggered each time a user sends a message to the WhatsApp bot:

const express = require("express");
const { sendResponse } = require("./utils/twilio");

const app = express();

const PORT = 5000;

app.use(express.json());
app.use(express.urlencoded({ extended: true }));

app.post("/", async (req, res) => {
  const { Body } = req.body;

  const greetMessage = `
  Hello and welcome to the European Football 2022/23 WhatsApp bot!\n\n You can get league standings and match reports about the top 5 European leagues for the 2022/23 season\n To get list of leagues type info
  `;

  const response = sendResponse(greetMessage);
  res.send(response);
});

// start the server on port 5000
app.listen(PORT, () =>
  console.log(`App listening at http://localhost:${PORT}`)
);

In the above route handler, the message sent by the user is received in the body of the request. The route handler imports and uses the sendResponse helper function to send a message back to the user and the message is formatted using TwiML before being returned.

Chatbot logic

The rest of the chatbot logic will be handled by some utility functions that would account for all possible user inputs. The chatbot would be able to:

  • Supply league standings for a particular league by looking for the “standings” string in the user’s prompt,
  • List match results for a game week by looking for the “week” string in the user’s prompt,
  • Give details about a particular match by looking for the “match” string in the user’s prompt.

Fetching league and match details

Inside the utils folder, you'll create three more files. First, you’ll need a file called fetcher.js that would handle the API calls to the Zeus API endpoint:

const axios = require("axios");
require("dotenv").config();

const config = {
  headers: {
    "X-RapidAPI-Key": process.env.RAPIDAPI_KEY,
    "X-RapidAPI-Host": "zeus-api1.p.rapidapi.com",
  },
};

const getLeaguesInfo = async () => {
  try {
    const response = await axios.get(
      "https://zeus-api1.p.rapidapi.com/competitions",
      config
    );
    return response.data.competitions;
  } catch (error) {
    return false;
  }
};

const getStandings = async (league) => {
  try {
    const response = await axios.get(
      `https://zeus-api1.p.rapidapi.com/competitions/${league}/standings/2022`,
      config
    );
    return response.data;
  } catch (error) {
    return false;
  }
};

const getMatches = async (week, leagueId) => {
  try {
    const response = await axios.get(
      `https://zeus-api1.p.rapidapi.com/competitions/${leagueId}/matches/2022/${week}`,
      config
    );
    return response.data;
  } catch (error) {
    return false;
  }
};

module.exports = { getLeaguesInfo, getStandings, getMatches };

This file contains three functions that would communicate with the Zeus API.

  • The getLeaguesInfo function gets all available competitions.
  • The getStandings function gets the current standings for a league.
  • The getMatches function gets the current matches for a particular game week in the specified league.

If an error occurs while getting the necessary information the functions return false so that the user can be informed by the helper function that would be created to parse the user’s message.

Formatting league and match details

Next, create a format.js file that'll convert the data received from the endpoint into a string that can be sent to the user:

const dayjs = require("dayjs");

const formatLeagues = (competitions) => {
  const leaguesStr = competitions.reduce(
    (accumulator, currentValue, currentIndex) => {
      const { id, name, description } = currentValue;

      return `${accumulator}\n \n${
        currentIndex + 1
      }. ${name} (${id}) - ${description}`;
    },
    ""
  );

  return `List of Leagues\n \n${leaguesStr}.\n \nTo get league standings for any league follow this format: STANDINGS _league-id_ eg. *STANDINGS PRL*\n \nTo get results for a week in any league follow this format: WEEK _league-id_  _week-number_ eg. *WEEK PRL 22*\n \nTo get match details for a game within a week in any league follow this format: MATCH _league-id_  _week-number_ _team-name_ eg. *MATCH PRL 22 Manchester City*`;
};

const formatStandings = ({ name, yearStart, yearEnd, standings }) => {
  const standingStr = standings.reduce((accumulator, currentValue) => {
    const { position, team, points, playedGames, wins, draws, loses, goalsFor, goalsAgainst} = currentValue;

    return `${accumulator}\n \n${position}. ${team?.name} - ${points} points - P ${playedGames} W ${wins} / D ${draws} / L ${loses} - GF ${goalsFor} / GA ${goalsAgainst}`;
  }, "");

  return `${name} - ${yearStart}/${yearEnd}\n \nStandings${standingStr}`;
};

const formatMatches = (matches, week, league) => {
  const resultsStr = matches.reduce((accumulator, currentValue) => {
    const { homeTeam, awayTeam, homeGoals, awayGoals } = currentValue;

    return `${accumulator}\n \n${homeTeam.name} ${homeGoals} - ${awayTeam.name} ${awayGoals}`;
  }, "");

  return `${league} - Week ${week}\n \nResults${resultsStr}`;
};

const formatMatchResult = (match, week, league) => {
  const { date, hour, homeTeam, awayTeam, homeGoals, awayGoals, stats } = match;

  const { ballPossessionHome, ballPossessionAway, goalAttemptsHome, goalAttemptsAway, offsidesHome, offsidesAway, foulsHome, foulsAway, totalPassesHome, totalPassesAway, attacksHome, attacksAway } = stats;

  const statsStr = `Possession: ${ballPossessionHome} - ${ballPossessionAway}\nGoal Attempts: ${goalAttemptsHome} - ${goalAttemptsAway}\nOffsides: ${offsidesHome} - ${offsidesAway}\nFouls: ${foulsHome} - ${foulsAway}\nPasses: ${totalPassesHome} - ${totalPassesAway}\nAttacks: ${attacksHome} - ${attacksAway}`;

  const formattedTime = dayjs(`${date}-${hour}`, "DD.MM.YYYY-HH:MM").format(
    "MMM DD YYYY  hh:mma"
  );

  return `${league} - Week ${week}\n \n*${homeTeam.name} ${homeGoals} - ${awayGoals} ${awayTeam.name}*\n \nMatch Date: ${formattedTime}\n \n${statsStr}`;
};

module.exports = { formatLeagues, formatStandings, formatMatches, formatMatchResult };

This file contains four functions that would format the returned data into a readable string for the user.

  • The formatLeague function receives an array of the available competitions and using the Array.reduce method converts it into a string.
  • The formatStandings function receives an object with the name, yearStart, yearEnd, and standings key and converts it into a string.
  • The formatMatches function receives an array of the matches for a particular game week, the week number, and the league name as arguments, and converts them into a string.
  • The formatMatchResult function receives an object containing the match details for a particular game within a game week, the week number, and the league name as arguments, and converts them into a string.

Parsing user’s request

Finally, create a controller.js file that will parse the message received from the user and choose which of the requests to make to get the necessary information:

const { getLeaguesInfo, getStandings, getMatches } = require("./fetcher");
const { formatLeagues, formatMatches, formatMatchResult, formatStandings } = require("./format");

const HELLO = "HELLO".toLowerCase();
const INFO = "INFO".toLowerCase();
const STANDINGS = "STANDINGS".toLowerCase();
const WEEK = "WEEK".toLowerCase();
const MATCH = "MATCH".toLowerCase();

const leagueObj = {
  PRL: "Premier League",
  LAL: "LaLiga",
  LI1: "Ligue 1",
  SEA: "Serie A",
  BUN: "Bundesliga",
};

const greetMessage = `
  Hello and welcome to the European Football 2022/23 WhatsApp bot!\n\nYou can get league standings and match reports about the top 5 European leagues for the 2022/23 season\nTo get list of leagues type info
  `;

const selectMatch = (matches, teamName) => {
  return matches.find((match) => {
    const { homeTeam, awayTeam } = match;
    const homeTeamName = homeTeam.name.replace(/-/g, " ").toLowerCase();
    const awayTeamName = awayTeam.name.replace(/-/g, " ").toLowerCase();
    const teamNameStr = teamName.replace(/-/g, " ").toLowerCase();

    if (
      homeTeamName.includes(teamNameStr) ||
      awayTeamName.includes(teamNameStr)
    )
      return match;
  });
};

const parseUserInput = async (body) => {
  if (!body) return greetMessage;
  const messageArr = body.split(" ");

  if (messageArr[0]?.toLowerCase().includes(HELLO)) {
    return greetMessage;
  }

  if (messageArr[0]?.toLowerCase().includes(INFO)) {
    const competitions = await getLeaguesInfo();
    if (!competitions) return "Sorry, we were unable to process this request";
    return formatLeagues(competitions);
  }

  if (messageArr[0]?.toLowerCase().includes(STANDINGS)) {
    const leagueId = messageArr[1]?.trim().toUpperCase();

    if (!leagueId) return "Please specify a league";

    const standings = await getStandings(leagueId);
    if (!standings) return "Sorry, we were unable to process this request";

    return formatStandings(standings);
  }

  if (messageArr[0]?.toLowerCase().includes(WEEK)) {
    const leagueId = messageArr[1]?.trim().toUpperCase();
    const week = messageArr[2]?.trim();

    if (!leagueId) return "Please specify a league";
    if (!week) return "Please specify a week";

    const matches = await getMatches(week, leagueId);
    if (!matches) return "Sorry, we were unable to process this request";

    const leagueName = leagueObj[leagueId.toUpperCase()];
    return formatMatches(matches.matches, matches.round, leagueName);
  }

  if (messageArr[0]?.toLowerCase().includes(MATCH)) {
    const [_, leagueId, week, ...rest] = messageArr;
    const team = rest.join(" ")?.trim();

    if (!leagueId) return "Please specify a league";
    if (!week) return "Please specify a week";
    if (!team) return "Please specify a team";

    const matches = await getMatches(week, leagueId);
    if (!matches) return "Sorry, we were unable to process this request";

    const match = selectMatch(matches.matches, team);

    if (!match) return `This match result was not found`;

    const leagueName = leagueObj[leagueId.toUpperCase()];
    return formatMatchResult(match, matches.round, leagueName);
  }
};

module.exports = { parseUserInput };

This file first of all creates some constants that would be needed by the functions in the file. It then defines two functions.

  • The selectMatch function receives an array of matches and the name of a team as arguments and then returns the object containing the match details for that team if contained in the array of matches.
  • The parseUserInput function receives the user’s message and determines which of the keywords the user has sent and then determines which of the other helper functions to call based on the user’s message. If an error occurred, the user is informed about it.

After creating the helper functions, it is now time to import and make use of them in the route handler. Go to the index.js file and replace the code in the route handler with the code below:

const { Body } = req.body;

const response = await parseUserInput(Body);
const message = sendResponse(response);

res.send(message);

Now your index.js file should look like this:

const express = require("express");

const { parseUserInput } = require("./utils/controller");
const { sendResponse } = require("./utils/twilio");

const app = express();

const PORT = 5000;

app.use(express.json());
app.use(express.urlencoded({ extended: true }));

app.post("/", async (req, res) => {
  const { Body } = req.body;

  const response = await parseUserInput(Body);
  const message = sendResponse(response);

  res.send(message);
});

// start the server on PORT 5000
app.listen(PORT, () =>
  console.log(`App listening at http://localhost:${PORT}`)
);

With the code for the chatbot done, it is now time to test the application.

Testing the application

The application is currently running on your computer at http://localhost:5000 but it is not accessible from the Internet. For Twilio to have access to the application and send and receive requests you would need to create a temporary public URL that is accessible to Twilio.

This would be done using the ngrok tool. Open a second terminal window and execute the following command:

ngrok http 5000

The command above tells ngrok to expose your application running at port 5000 over the internet. You should see an output like the one below which gives you a Forwarding URL through which your app can be accessed over the web. You would be making use of the secure URL, which starts with https://

Terminal showing the https forwarding URL generated by ngrok

Set up Twilio Whatsapp Sandbox

Twilio provides a WhatsApp sandbox that allows you to easily develop and test your application. Navigate to the WhatsApp sandbox configuration page in the Twilio Console. On the Sandbox tab which should look like the image below, make a note of the Twilio phone number and the join code:

Twilio Whatsapp Sandbox page

Switch to the Sandbox Settings tab and set up the endpoint URL that Twilio will make use of to forward messages sent or received from WhatsApp. Copy and paste the public forwarding URL obtained earlier from ngrok into the When a message comes in field and then click the Save button:

Twilio Whatsapp Sandbos Settings tab

Your application is now ready to be tested. Open WhatsApp on your personal device and send a message to the phone number indicated on the Twilio WhatsApp sandbox. The chatbot would respond based on the messages you send.

Football bot response

Football bot response

Conclusion

In this tutorial you have created a simple chatbot that returns information about football leagues, matches, and match details based on certain keywords. This was implemented using Express, Zeus API, and the Twilio API for WhatsApp.

The chatbot’s functionality can be extended further. For example by allowing users to state the year that they want league standings and match results for. All the code for this tutorial can be found on GitHub here.

I hope you learnt something from this tutorial and are inspired to build some amazing chatbots of your own.

Anderson Osayerie is an experienced Web Developer and technical writer with a proven track record of crafting and creating products for global companies. He is very interested in creating aesthetically pleasing and functional web applications. He can be reached via the following channels:

LinkedIn - https://www.linkedin.com/in/anderson-osayerie/

Github - https://github.com/andemosa

Twitter - https://twitter.com/andemosa