Build a WhatsApp Synonyms Game Bot with Python, Flask and Twilio

November 30, 2020
Written by
Ayoyinka Obisesan
Contributor
Opinions expressed by Twilio contributors are their own

Build a WhatsApp Synonyms Game Bot with Python, Flask and Twilio

A research from 2013 shows that games can increase learning outcomes by two grade levels. The effect of games on learning can never be over-emphasized. So wouldn’t it be a good idea to build a word game to learn more about a language? I think it will be fun. Building the game on WhatsApp, an application with over 2 billion users around the globe, makes it even more interesting!

In this tutorial, I am going to do a walkthrough on how to build your own word game on WhatsApp. For the purpose of this tutorial, the word game will be centered around synonyms. The game will present you with words, and you will need to enter synonyms. Each time you enter a correct synonym you earn a point. The game continues until you make a mistake, at which you point your score is given to you. A generic approach will be adopted in order to make this extensible to other word games (antonyms, etc.) that you may want to build in the future.

Game demo

Prerequisites

If you wish to follow along with the tutorial, you will need to have the following components:

  • A Twilio account. Create one if you don’t have it yet. This is important for the Twilio phone number which will be used to implement the game.
  • A Merriam-Webster's developer account. Create one if you don’t have it yet. An API key will be provided on sign-up.
  • A mobile phone with WhatsApp installed to test the game.
  • Python 3.6 or newer. Download the Python installer for your operating system here.
  • ngrok. Since Twilio needs to connect to a public URL, we will be using ngrok (a free tool) to put our web app running on our local machine temporarily on the Internet. Download the ngrok installer for your operating system here.

Set up Twilio WhatsApp sandbox

Twilio provides a WhatsApp sandbox to enable easy development and testing of our applications. To connect to the sandbox from your smartphone, select Programmable Messaging from your Twilio Console, click “Try it Out” and finally Try WhatsApp. The WhatsApp sandbox page shows the sandbox number assigned to your account, and a join code as shown below:

Twilio WhatsApp Sandbox

To enable the WhatsApp sandbox for your smartphone, you need to send a WhatsApp message with the given code (which begins with the word join, followed by a randomly generated two-word phrase) to the number assigned to your account. Twilio responds with confirmation that your mobile number is connected to the sandbox after it receives the code. This step needs to be repeated for any additional phones you’d like to have connected to your sandbox. Also, note that sandbox sessions expire after 3 days and you will need to send the code again to the number in order to get reconnected.

Create a Python virtual environment

It’s best practice to create separate virtual environments for different python projects. With that in mind, we should create one for our game. To start with, we will make a new directory for our project, and create the virtual environment inside it. Then we will activate the virtual environment and install the following packages needed for our game:

For Windows users following along, enter the following commands in a command prompt window to complete these tasks:

$ md synonymsgame-venv
$ cd synonymsgame-venv
$ python -m venv synonymsgame-venv
$ synonymsgame-venv\Scripts\activate
(synonymsgame-venv) $ pip install flask twilio requests

If you are on a Unix or Mac OS system, enter the following commands on a terminal window:

$ mkdir synonymsgame
$ cd synonymsgame
$ python3 -m venv synonymsgame-venv
$ source synonymsgame-venv/bin/activate
(synonymsgame-venv) $ pip install flask twilio requests 

For reference purposes, below you can see the versions of the installed packages and their dependencies that were used to successfully develop and test the game. Here is a link to a requirements.txt file on GitHub with the same content.

certifi==2020.11.8
chardet==3.0.4
click==7.1.2
Flask==1.1.2
idna==2.10
itsdangerous==1.1.0
Jinja2==2.11.2
MarkupSafe==1.1.1
PyJWT==1.7.1
pytz==2020.4
requests==2.24.0
six==1.15.0
twilio==6.47.0
urllib3==1.25.11
Werkzeug==1.0.1

Getting started with the Merriam-Webster API

By now you should have created a Merriam-Webster developer account, and should have gotten a key for the Collegiate Thesaurus API. Below is a screenshot of the API keys page:

Merriam-Webster's API keys page

The picture below shows the available APIs from Merriam-Webster and highlighted in red the place where you would see the API key that we will be using for our application.

The overview for this API is located here. In particular this page shows the request URL to access the API:

Merriam-Webster API resource URL

Next, I am going to show some examples of how to work with the endpoint from the command prompt with Python. If you want to follow along, make sure that your Python virtual environment is activated.

Then add your Merriam-Webster API key as an environment variable. For Windows users:

(venv) $ set API_KEY=YOUR-API-KEY

For Unix or Mac OS users:

(venv) $ export API_KEY=YOUR_API_KEY

Type python3 in your command prompt or terminal window to start a Python interactive session. You should see the Python version information followed by the >>> Python prompt:

Python 3.9.0 (tags/v3.9.0:9cf6752, Oct  5 2020, 15:34:40) [MSC v.1927 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>>     

Now, I would show how to work with the API from the python terminal.

>>> import os, requests
>>> api_key = os.environ['API_KEY']
>>>
>>> random_word = 'synonym'
>>> url = "https://www.dictionaryapi.com/api/v3/references/thesaurus/json/" + random_word + "?key=" + api_key
>>> response = requests.get(url)
>>> response.json()[0]['meta']['syns'][0]
['carbon copy', 'copy', 'duplicate', 'replica', 'analogue', 'counterpart']
>>>
>>> random_word = 'immortal'
>>> url = "https://www.dictionaryapi.com/api/v3/references/thesaurus/json/" + random_word + "?key=" + api_key
>>> response = requests.get(url)
>>> response.json()[0]['meta']['syns'][0]
['ceaseless', 'dateless', 'deathless', 'endless', 'eternal', 'everlasting', 'permanent', 'perpetual', 'undying', 'unending']

We started the Python session by importing os and requests. We will use os to retrieve the API key environment variable, and requests to make HTTP requests to the Merriam-Webster API.

We then set the word “synonym” as an example first word, and then create a url variable with the request URL, including the desired word and the API key, and following the guidelines in the API documentation.

Next we send a GET request to the endpoint URL we just created. This is done by calling the requests.get() function and passing the URL as an argument. The requests package returns a response object, which we capture in the response variable.

The response.json() method returns the JSON data returned by the API as a Python dictionary. From there we need to filter the data and only keep the synonyms, which is what we are interested in. This is done with the expression response.json()[0]['meta']['syns'][0].

In the second half of the session we repeat the process using the word “immortal”.

Generating random words

In the example session from the previous section we manually selected words to be searched for. We obviously want to automate this process and have the game select random words on its own. To do this we are going to use a JSON file containing about 1000 common English words. The JSON file can be found in this GitHub repository and here is the raw file. Thanks to Darius Kazemi who has made this repository available.

From a Python session we can grab this word list as follows:

>>> import requests
>>> url = "https://raw.githubusercontent.com/dariusk/corpora/master/data/words/common.json"
>>> response = requests.get(url)
>>> words = response.json()['commonWords']

Once we have the list of words, we can select one at random using Python’s random module:

>>> import random
>>> random_word = words[random.randint(1, len(words))]
>>> print(random_word)
package

The Twilio API for WhatsApp

The Twilio API for WhatsApp uses a webhook to notify an application when there is an incoming message. For our game application, we need to define an endpoint that is going to be configured as this webhook so that Twilio can communicate with our application. For this purpose, we would be using the Flask framework which makes it easy to define a webhook.

If you are not familiar with the Flask framework, its documentation has a quick start section that should bring you up to speed quickly. If you want a more in-depth learning resource then I recommend you follow Miguel Grinberg’s Flask Mega-Tutorial.

Below you can see the standard format to define a webhook for an application:

from flask import Flask

app = Flask(__name__)

@app.route('/synonymsgame', methods=['POST'])
def bot():
    # TODO: webhook logic

The above code defines a /synonymsgame endpoint that supports POST requests. The body of the function bot() will contain the logic to process the incoming message and return a response.

The first thing that needs to be done in our game application is to obtain the message entered by the user on WhatsApp. This message comes in the payload of the POST request with a key of ’Body’ and can be accessed through Flask’s request object:

from flask import request

incoming_msg = request.values.get('Body', '').strip().lower()

The message received is stripped of any whitespace and converted to lowercase to ensure that we don’t have to worry about all the different ways a word can appear when you introduce case variations.

The response that Twilio expects from the webhook needs to be given in TwiML or Twilio Markup Language, which is an XML-based language. However, the Twilio helper library for Python comes with classes that make it easy to create this response without having to create XML directly. Below you can see the code necessary to create a response that includes text that will be shown to the user on WhatsApp:

from twilio.twiml.messaging_response import MessagingResponse

resp = MessagingResponse()
msg = resp.message()
msg.body('hello world')

Creating the Game

This section will bring all the details we have covered in the previous sections together. That is, now we will be implementing the actual logic for the game.

Basic endpoint structure

Below you can see the beginning of synonymsgame.py, where we import all the dependencies and define the Flask endpoint:

from flask import Flask, session, request
import requests
from twilio.twiml.messaging_response import MessagingResponse
import random, os

app = Flask(__name__)

app.secret_key = os.environ['SECRET_KEY']
api_key = os.environ['API_KEY']


@app.route('/synonymsgame', methods=['POST'])
def bot():
    incoming_msg = request.values.get('Body', '').strip().lower()
    resp = MessagingResponse()
    msg = resp.message()

First, we make the necessary imports, and create our app application instance. Flask applications require a secret_key definition to work with session objects, so we define it by getting the value from the environment variable with the name SECRET_KEY. The api_key variable has been discussed earlier, this is the developer key to access the Merriam-Webster API, and is also imported from an environment variable.

Next we define the webhook and the starter code for the game logic, which gets the message from the user and defines a message response object that we will use to return messages to the user.

Handling new games

Continuing with the logic in the bot() function, now we can check if a new game is starting. This happens when the user writes “new game” in WhatsApp.

    if incoming_msg == "new game":
        random_word, synonyms = get_word()
        session["num_correct"] = 0
        session["random_word"] = random_word
        session["synonyms"] = synonyms
        msg.body("First word: " + random_word + '. Type a synonym!')

To initialize a new game we obtain a new random word and its synonyms. We do not have a get_word() function yet, but we’ll write it soon.

Since we need to remember the state of the game as we interact with the user on WhatsApp, we store a number of things in the session variable from Flask:

  • num_correct, which represents the number of correct answers, initially set to 0.
  • random_word, the selected random word.
  • synonyms, the synonyms of the selected random word

We finally generate a message to the user telling them what the word is.

Handling ongoing games

If the endpoint is invoked in the middle of a game, we can retrieve the game state from the variables we stored in the session object. The following code continues with the body of the bot() function and handles an answer from the user.

    elif session.get('random_word'):
        random_word = session["random_word"]
        synonyms = session["synonyms"]
 
        if incoming_msg in synonyms:
            session["num_correct"] += 1
            session.pop("random_word")
            session.pop("synonyms")
            random_word, synonyms = get_word()
            session["random_word"] = random_word
            session["synonyms"] = synonyms
            msg.body("Correct!, next word: " + random_word + '. Type a synonym!')
 
        else:
            session.pop("random_word")
            session.pop("synonyms")
            score = session.pop("num_correct")
            msg.body("Game over! " + "You got " + str(score) + " questions correctly" + \
                     "\n\nSynonyms for " + random_word + " are <" + ", ".join(synonyms) + ">" + \
                     "\n\nType <new game> to start a new game")
 
    else:
        msg.body("Type <new game> to start a new game")

    return str(resp)

Here we restore the random word and synonyms from the user session, and then check if the word typed by the user on WhatsApp is in our list of synonyms.

If the word is a valid synonym, then we increment the num_correct variable in the session, replace the word and its synonyms with a new randomly generated word, and inform the user what the next word is.

If the word entered by the user is not in our list, then the game is over. In that case we clean up the session and return a final message to the user with the score.

The big if statement that controls the game logic has a third and final section, which applies when the user did not type “new game” and we do not have the session variables that indicate that we have a game in progress. In that case we just let the user know what the command to start a new game is.

At the very end of the function we return the resp object, in which we have created our response to the user.

Generating random words

The game logic we wrote in the previous section uses the get_word() auxiliary function. This function chooses a random word from the list of common words in JSON format, and then obtains its synonyms from the Merriam-Webster API. The function returns the word and its synonyms in a tuple.

Below you can see the definition of the function. Add it at the end of the synonymsgame.py file:

def get_word():
    url = "https://raw.githubusercontent.com/dariusk/corpora/master/data/words/common.json"
    res = requests.request("GET", url)
    results = res.json()['commonWords']
    random_word = results[random.randint(1, len(results))]
    url = "https://www.dictionaryapi.com/api/v3/references/thesaurus/json/" + random_word + "?key=" + api_key
    response = requests.get(url)
    synonyms = response.json()[0]['meta']['syns'][0]
    return random_word, synonyms

Starting the web server

The final piece of our application is the lines that run the Flask server. These go at the bottom of the synonymsgame.py file:

if __name__ == "__main__":
    app.run()

Everything Together

Now you have seen all the aspects of the chatbot implementation, so we are ready to integrate all the pieces into the complete chatbot service. If you haven’t been building the application along, you can now copy the complete code shown below into the synonymsgame.py file:

from flask import Flask, session, request
import requests
from twilio.twiml.messaging_response import MessagingResponse
import random, os

app = Flask(__name__)

app.secret_key = os.environ['SECRET_KEY']
api_key = os.environ['API_KEY']


@app.route('/synonymsgame', methods=['POST'])
def bot():
    incoming_msg = request.values.get('Body', '').strip().lower()
    resp = MessagingResponse()
    msg = resp.message()

    if incoming_msg == "new game":
        random_word, synonyms = get_word()
        session["num_correct"] = 0
        session["random_word"] = random_word
        session["synonyms"] = synonyms
        msg.body("First word: " + random_word + '. Type a synonym!')

    elif session.get('random_word'):
        random_word = session["random_word"]
        synonyms = session["synonyms"]
 
        if incoming_msg in synonyms:
            session["num_correct"] += 1
            session.pop("random_word")
            session.pop("synonyms")
            random_word, synonyms = get_word()
            session["random_word"] = random_word
            session["synonyms"] = synonyms
            msg.body("Correct!, next word: " + random_word + '. Type a synonym!')
 
        else:
            session.pop("random_word")
            session.pop("synonyms")
            score = session.pop("num_correct")
            msg.body("Game over! " + "You got " + str(score) + " questions correctly" + \
                     "\n\nSynonyms for " + random_word + " are <" + ", ".join(synonyms) + ">" + \
                     "\n\nType <new game> to start a new game")
 
    else:
        msg.body("Type <new game> to start a new game")

    return str(resp)


def get_word():
    url = "https://raw.githubusercontent.com/dariusk/corpora/master/data/words/common.json"
    res = requests.request("GET", url)
    words = res.json()['commonWords']
    random_word = words[random.randint(1, len(words))]
    url_ = "https://www.dictionaryapi.com/api/v3/references/thesaurus/json/" + random_word + "?key=" + api_key
    response = requests.request("GET", url_)
    synonyms = response.json()[0]['meta']['syns'][0]
    return random_word, synonyms


if __name__ == "__main__":
    app.run()

Testing the Game

Final lap! Before you start the game make sure that you have defined the two environment variables that the game needs. As a reminder, their names are SECRET_KEY and API_KEY. On a Windows machine, you can set them as follows:

(venv) $ set SECRET_KEY=something-secret
(venv) $ set API_KEY=YOUR-API-KEY

If you use a Unix or Mac OS computer, then use the following commands:

(venv) $ export SECRET_KEY=something-secret
(venv) $ export API_KEY=YOUR-API-KEY

The value that you choose for the SECRET_KEY variable can be anything you like. Flask will use this to generate a cryptographic signature for the user sessions, making them secure.

After copying the code from the previous section into the *synonymsgame.py* file, you can start the game by running python synonymsgame.py and making sure you do this while the Python virtual environment is activated and the environment variables are set. The output should be something like this:

Flask application running

The service is now running as a private service on port 5000 inside your computer and will sit there waiting for incoming connections.

As discussed earlier, we are going to use ngrok to make the application reachable by Twilio over the Internet. Open a second terminal window and run ngrok http 5000 to allocate a temporary public domain that redirects HTTP requests to our local port 5000. The output of ngrok is:

ngrok screenshot

Note the two lines that start with “Forwarding”. These show the http:// and https:// URLs that are temporarily mapped to the application running locally.

The final step is to tell Twilio to use this URL to send incoming message notifications. To achieve this, go back to the Twilio Console, click on Programmable Messaging, then on Settings, and finally on WhatsApp Sandbox Settings. Copy the https:// URL from the ngrok output and then paste it on the “When a message comes in” field. Since our chatbot is exposed under the /synonymsgame URL, append that at the end of the root ngrok URL. Make sure the request method is set to HTTP Post and don’t forget to click the Save button at the bottom of the page to record these changes.

Set whatsapp webhook

Now you can start playing the game from the smartphone that you connected to the sandbox. Send new game to the sandbox number to begin!

Game demo

 

Conclusion

Games provide a 23% gain over traditional learning. A word game like this can be useful in learning new words to improve our vocabulary. Also, this can be useful to prepare for exams requiring verbal reasoning such as the GRE® General Test.

I hope this tutorial was useful and you now have a better idea of how to build and extend a word game for WhatsApp. I would love to see you build some more interesting word games. If you want to learn more about the Twilio API for WhatsApp, here is a link to the documentation.

Ayoyinka Obisesan is a Graduate student at Carnegie Mellon University Africa.