Sign up for updates!

Hi! 👋 Please sign up to get notified when I release the source code for this book on GitHub. Till then, I would greatly appreciate if you can submit all the grammatical, prose and code related issues here.

I post about once every couple of months. No spam, I promise

6 Making a Reddit + Facebook Messenger Bot

Hi everyone! This chapter’s project is a Facebook messenger bot which serves you fresh memes, motivational posts, jokes, and shower thoughts. This project will provide an introduction to the general approach and tools you can use to make a messenger bot. Let’s get into it!

Messenger Bot

Fig. 6.1 Final bot in action

Tech Stack

For this bot, we will be making use of the following:

  • Flask framework for coding up the backend

  • Heroku for hosting your code online for free

  • Reddit as a data source (because it gets new submissions every second!)

6.1 Creating a Reddit app

Since you will be leveraging Facebook, Heroku, and Reddit, you’ll want to start by making sure that you have an account on all three of these platforms. Next, you need to create a Reddit application using this link.

Reddit app

Fig. 6.2 Creating a new app on Reddit

In Fig. 6.2 you can check out the “motivation” app, which is already completed. Click on “create another app…” and follow the on-screen instructions (Fig. 6.3).

describe the app

Fig. 6.3 Filling out the new app form

For this project, you won’t be using the ‘about’ URL or ‘redirect’ URI, so it’s okay to leave them blank. For production apps, it’s best to put in something related to your project in the description. This way, if you start making a lot of requests and Reddit notices, they can check the about page for your app and act in a more informed manner.

Now that your app is created, you need to save the client_id and client_secret in a safe place (Fig. 6.4).

client id and secret

Fig. 6.4 Make note of client_id and client_secret

Now you can start working on the Heroku app!

6.2 Creating an App on Heroku

Go to this dashboard URL and create a new application. You might remember using the command-line to create a new app in the FIFA Twilio bot chapter. In this chapter, we will create the app using the Heroku web-UI.

creating app on heroku

Fig. 6.5 Create an app on Heroku

First, give your application a unique name (Fig. 6.6). On the next page, (Fig. 6.7), click on “Heroku CLI” and download the latest Heroku CLI for your operating system. Follow the on-screen install instructions and come back once it has been installed.

naming the app

Fig. 6.6 Let’s name the app

finalizing creation

Fig. 6.7 Final step of new app creation process

6.3 Creating a basic Python application

First, create a new directory, then follow these instructions to add a virtual environment:

$ python -m venv env
$ source env/bin/activate

Then, instead of starting our code completely from scratch, we will use some starter code which already has the basics of bot initialization in place. Don’t worry, we will step through what each part is doing.

The below code is taken from Konstantinos Tsaprailis’s website.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
from flask import Flask, request
import json
import requests
import os

app = Flask(__name__)

# This needs to be filled with the Page Access Token that will be provided
# by the Facebook App that will be created.
PAT = 'PAGE-ACCESS-TOKEN-GOES-HERE'

@app.route('/', methods=['GET'])
def handle_verification():
    print("Handling Verification.")
    if request.args.get('hub.verify_token', '') == 'my_voice_is_my_password_verify_me':
        print("Verification successful!")
        return request.args.get('hub.challenge', '')
    else:
        print("Verification failed!")
        return 'Error, wrong validation token'

@app.route('/', methods=['POST'])
def handle_messages():
    print("Handling Messages")
    payload = request.get_data()
    print(payload)
    for sender, message in messaging_events(payload):
        print("Incoming from %s: %s" % (sender, message))
        send_message(PAT, sender, message)
    return "ok"

def messaging_events(payload):
    """Generate tuples of (sender_id, message_text) from the
    provided payload.
    """
    data = json.loads(payload)
    messaging_events = data["entry"][0]["messaging"]
    for event in messaging_events:
        if "message" in event and "text" in event["message"]:
            yield event["sender"]["id"], event["message"]["text"].encode('unicode_escape')
        else:
            yield event["sender"]["id"], "I can't echo this"


def send_message(token, recipient, text):
    """Send the message text to recipient with id recipient.
    """

    r = requests.post("https://graph.facebook.com/v3.3/me/messages",
        params={"access_token": token},
        data=json.dumps({
            "recipient": {"id": recipient},
            "message": {"text": text.decode('unicode_escape')}
        }),
        headers={'Content-type': 'application/json'})
    if r.status_code != requests.codes.ok:
        print(r.text)

if __name__ == '__main__':
    port = int(os.environ.get('PORT', 5000))
    app.run(host='0.0.0.0', port=port)

In this code, we have a handler for GET and POST requests to the / endpoint. Let’s break down the code a bit and understand what’s going on. In order to make sure our bot only responds to requests originating from Facebook, Facebook appends a verify_token arg to the GET request to / endpoint. In the handle_verification function, we are checking the value of this parameter. The value my_voice_is_my_password_verify_me is completely made up. We will provide this value to Facebook ourselves from the online developer console. We will talk about that later.

The handle_messages function handles the POST requests from Facebook, which contain information about each new message our bot receives. It just echoes back whatever it receives from the user.

We will be modifying the file according to our needs.

In summary, a Facebook bot works like this:

  1. Facebook sends a request to our server whenever a user messages our page on Facebook

  2. We respond to Facebook’s request and store the id of the user and the message which was sent to our page

  3. We respond to user’s message through Graph API using the stored user id and message id

A detailed breakdown of the above code is available on this website. Note that the version of the code in this chapter has been modified slightly to make it Python 3 compatible and use the newer version of the Graph API. For the purpose of this project, we will mainly be focusing on Reddit integration and how to use the Postgres Database on Heroku.

Before moving further, let’s deploy the above Python code onto Heroku. For that, you should create a local Git repository. Follow the following steps:

1
2
3
4
5
$ mkdir messenger-bot
$ cd messenger-bot
$ touch requirements.txt app.py Procfile runtime.txt
$ python -m venv env
$ source env/bin/activate

Execute the above commands in a terminal and put the above Python code into the app.py file. Put the following into Procfile:

web: python app.py

Now you need to tell Heroku which Python libraries your app will need to function properly. Those libraries will need to be listed in the requirements.txt file. We can fast-forward this a bit by copying the requirements from this post. Put the following lines into requirements.txt file and you should be good to go.

1
2
3
4
5
6
7
8
click==7.1.2
Flask==1.1.2
gunicorn==20.0.4
itsdangerous==1.1.0
Jinja2==3.0.0a1
MarkupSafe==2.0.0a1
requests==2.24.0
Werkzeug==1.0.1

Note

The version numbers listed here may not match what you are using, but the behavior should be the same.

Add the following code to the runtime.txt file:

python-3.6.5

Now your directory should look something like this:

$ ls
Procfile         app.py          env          requirements.txt
runtime.txt

Now you’re ready to create a Git repository, which can then be pushed onto Heroku servers. Now carry out the following steps:

  • Login into Heroku

  • Create a new local git repository

  • Commit everything into the new repo

  • Push the repo to Heroku

The commands required for this are listed below:

1
2
3
4
5
6
$ git init
$ heroku create
$ heroku apps:rename custom_project_name
$ git add .
$ git commit -m "Initial commit"
$ git push heroku master

Note

Don’t forget to change custom_project_name to something unique.

You can look back to the FIFA bot chapter to review what each command is doing. Save the URL which is output after running the Heroku rename command. This is the URL of your Heroku app. You will need it in the next step, where you’ll create the Facebook app.

6.4 Creating a Facebook App

First, you need a Facebook page. It is a requirement by Facebook to supplement every app with a relevant page, so you’ll need to create one before moving on.

Now you need to register a new app. Go to this app creation page and follow the instructions below.

Click on Add a New App

Fig. 6.8 Click on Add a New App

Give the app a name and email

Fig. 6.9 Give the app a name and email

Note

The app creation UI might be a bit different when you follow this tutorial since Facebook regularly updates the UI. However, it should still be relatively similar to what is shown here.

Go to Add Product

Fig. 6.10 Go to Add Product

Click on Get Started

Fig. 6.11 Click on Get Started

Generate and save the page access token

Fig. 6.12 Generate and save the page access token

Fill out the New Page Subscription form

Fig. 6.13 Fill out the New Page Subscription form

Fig. 6.14 Link a page to the app

Now head over to your app.py file and replace the PAT variable assignment on line 9 like this:

PAT = os.environ.get('FACEBOOK_TOKEN')

Next, run the following command in the terminal (replace ***** with the token you recieved from the previous step):

$ heroku config:set FACEBOOK_TOKEN=**************

Commit everything and push the code to Heroku.

$ git commit -am "Added in the PAT"
$ git push heroku master

Now, if you go to the Facebook page and send a message to the page you configured above, you will receive your own message as a reply from the page. This shows that everything we have done so far is working. If this doesn’t work as expected, check your Heroku logs to debug. This should give you some clues about what is going wrong. After checking the logs, a quick Google search will help you resolve the issue. You can access the logs like this:

$ heroku logs -t

Note

Only your msgs will be replied to by the Facebook page. If any other random user messages the page, their messages will not be replied to by the bot. This is because the bot is currently not approved by Facebook. However, if you want to enable a couple of users to test your app, you can add them as testers. You can do so by going to your Facebook app’s developer page and following the on-screen instructions.

6.5 Getting data from Reddit

We will be using data from the following subreddits:

First of all, let’s install Reddit’s Python library praw. This can be done by typing the following command in the terminal:

$ pip install praw

Now let’s test some Reddit goodness in a Python shell. The docs explain how to access Reddit and subreddits. Now is the best time to grab the client_id and client_secret, which you received from Reddit.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
$ python
Python 3.8.3 (default, Jul 2 2020, 09:23:15)
[Clang 10.0.1 (clang-1001.0.46.3)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import praw
>>> reddit = praw.Reddit(client_id='**********',
... client_secret='*****************',
... user_agent='my user agent')
>>>
>>> submissions = list(reddit.subreddit("GetMotivated").hot(limit=None))
>>> submissions[-4].title
u'[Video] Hi, Stranger.'

Note

Don’t forget to add in your own client_id and client_secret in place of ****

Let’s review the important bits here. We are using limit=None because you want to get back as many posts as you can. Initially, this might feel like overkill- but you will quickly see that when a user starts using the Facebook bot frequently, you’ll run out of new posts if we limit ourselves to just 10 or 20 posts. An additional constraint which we will add is that we will only use the image posts from GetMotivated and Memes and only text posts from Jokes and ShowerThoughts. Due to this constraint, only one or two posts from top 10 hot posts might be useful to us, since we will be filtering out other types of content, like videos.

Now that you know how to access Reddit using the Python library, you can go ahead and integrate it into your app.py.

6.6 Putting everything together

First, we’ll need to add some additional libraries into requirements.txt, so that it looks something like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
$ cat requirements.txt
click==7.1.2
Flask==1.1.2
gunicorn==20.0.4
itsdangerous==1.1.0
Jinja2==3.0.0a1
MarkupSafe==2.0.0a1
requests==2.24.0
Werkzeug==1.0.1
whitenoise==5.2.0
praw==7.1.0

If you only wanted to send the user an image or text taken from Reddit, it wouldn’t be very difficult. In the send_message function, you could have something like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import praw
# ...

def send_message(token, recipient, text):
    """Send the message text to recipient with id recipient.
    """
    if b"meme" in text.lower():
        subreddit_name = "memes"
    elif b"shower" in text.lower():
        subreddit_name = "Showerthoughts"
    elif b"joke" in text.lower():
        subreddit_name = "Jokes"
    else:
        subreddit_name = "GetMotivated"
    # ....

    if subreddit_name == "Showerthoughts":
        for submission in reddit.subreddit(subreddit_name).hot(limit=None):
            payload = submission.url
            break
    # ...

    r = requests.post("https://graph.facebook.com/v3.3/me/messages",
            params={"access_token": token},
            data=json.dumps({
                "recipient": {"id": recipient},
                "message": {"attachment": {
                              "type": "image",
                              "payload": {
                                "url": payload
                              }}
            }),
            headers={'Content-type': 'application/json'})
    # ...

But, there is one issue with this approach. How will we know whether a user has been sent a particular image/text or not? We need some kind of id for each image/text we send the user so that we don’t send the same post twice. In order to solve this issue, we are going to use Postgresql (a database tool) and Reddit’s post ids (every post on Reddit has a special id).

In this approach, we will be using two tables, with a many-to-many relation between the tables. If you don’t know what a many-to-many relationship is, you can read this nice article by Airtable. Our tables will be keeping track of two things:

  • Users

  • Posts

Let’s first define the tables in our code, and then go into how they work. The following code should go into the app.py file:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
from flask_sqlalchemy import SQLAlchemy

# ...
app.config['SQLALCHEMY_DATABASE_URI'] = os.environ['DATABASE_URL']
db = SQLAlchemy(app)

# ...
relationship_table=db.Table('relationship_table',
    db.Column('user_id', db.Integer,db.ForeignKey('users.id'), nullable=False),
    db.Column('post_id',db.Integer,db.ForeignKey('posts.id'),nullable=False),
    db.PrimaryKeyConstraint('user_id', 'post_id') )

class Users(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(255),nullable=False)
    posts=db.relationship('Posts', secondary=relationship_table, backref='users' )

    def __init__(self, name):
        self.name = name

class Posts(db.Model):
    id=db.Column(db.Integer, primary_key=True)
    name=db.Column(db.String, unique=True, nullable=False)
    url=db.Column(db.String, nullable=False)

    def __init__(self, name, URL):
        self.name = name
        self.url = url

The user table has two fields. The name field will contain the id sent with the Facebook Messenger Webhook request. The posts field will be linked to the other table, “Posts”. The Posts table has name and URL fields. The name field will be populated by the Reddit submission id and the URL will be populated by the URL for that post. You don’t technically need to have the URL field, but it may be useful for other versions of the project, which you may want to make in the future.

This is how the final code will work:

  • We request a list of posts from a particular subreddit using the following code:

    reddit.subreddit(subreddit_name).hot(limit=None)
    

    This returns a generator object, so we don’t need to worry about memory

  • We will check whether the particular post has already been sent to the user or not

  • If the post has been sent in the past, we will continue requesting more posts from Reddit until we find a fresh post

  • If the post has not been sent to the user, we will send the post and break out of the loop

The final code of the app.py file is this:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
from flask import Flask, request
import json
import requests
from flask_sqlalchemy import SQLAlchemy
import os
import praw

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = os.environ.get('DATABASE_URL')
db = SQLAlchemy(app)
reddit = praw.Reddit(client_id='**********',
                     client_secret='************************',
                     user_agent='my user agent')

# This needs to be filled with the Page Access Token that will be provided
# by the Facebook App that will be created.
PAT = '****************'

quick_replies_list = [{
    "content_type":"text",
    "title":"Meme",
    "payload":"meme",
},
{
    "content_type":"text",
    "title":"Motivation",
    "payload":"motivation",
},
{
    "content_type":"text",
    "title":"Shower Thought",
    "payload":"Shower_Thought",
},
{
    "content_type":"text",
    "title":"Jokes",
    "payload":"Jokes",
}]

@app.route('/', methods=['GET'])
def handle_verification():
    print("Handling Verification.")
    if request.args.get('hub.verify_token', '') == 'my_voice_is_my_password_verify_me':
        print("Verification successful!")
        return request.args.get('hub.challenge', '')
    else:
        print("Verification failed!")
        return 'Error, wrong validation token'

@app.route('/', methods=['POST'])
def handle_messages():
    print("Handling Messages")
    payload = request.get_data()
    print(payload)
    for sender, message in messaging_events(payload):
        print("Incoming from %s: %s" % (sender, message))
        send_message(PAT, sender, message)
    return "ok"

def messaging_events(payload):
    """Generate tuples of (sender_id, message_text) from the
    provided payload.
    """
    data = json.loads(payload)
    messaging_events = data["entry"][0]["messaging"]
    for event in messaging_events:
        if "message" in event and "text" in event["message"]:
            yield event["sender"]["id"], event["message"]["text"].encode('unicode_escape')
        else:
            yield event["sender"]["id"], "I can't echo this"


def send_message(token, recipient, text):
    """Send the message text to recipient with id recipient.
    """
    if b"meme" in text.lower():
        subreddit_name = "memes"
    elif b"shower" in text.lower():
        subreddit_name = "Showerthoughts"
    elif b"joke" in text.lower():
        subreddit_name = "Jokes"
    else:
        subreddit_name = "GetMotivated"

    myUser = get_or_create(db.session, Users, name=recipient)

    if subreddit_name == "Showerthoughts":
        for submission in reddit.subreddit(subreddit_name).hot(limit=None):
            if (submission.is_self == True):
                query_result = (
                    Posts.query
                        .filter(Posts.name == submission.id).first()
                )
                if query_result is None:
                    myPost = Posts(submission.id, submission.title)
                    myUser.posts.append(myPost)
                    db.session.commit()
                    payload = submission.title
                    break
                elif myUser not in query_result.users:
                    myUser.posts.append(query_result)
                    db.session.commit()
                    payload = submission.title
                    break
                else:
                    continue  

        r = requests.post("https://graph.facebook.com/v2.6/me/messages",
            params={"access_token": token},
            data=json.dumps({
                "recipient": {"id": recipient},
                "message": {"text": payload,
                            "quick_replies":quick_replies_list}
                #"message": {"text": text.decode('unicode_escape')}
            }),
            headers={'Content-type': 'application/json'})
    
    elif subreddit_name == "Jokes":
        for submission in reddit.subreddit(subreddit_name).hot(limit=None):
            if ((submission.is_self == True) and 
                    ( submission.link_flair_text is None)):
                query_result = (
                    Posts.query
                    .filter(Posts.name == submission.id).first()
                )
                if query_result is None:
                    myPost = Posts(submission.id, submission.title)
                    myUser.posts.append(myPost)
                    db.session.commit()
                    payload = submission.title
                    payload_text = submission.selftext
                    break
                elif myUser not in query_result.users:
                    myUser.posts.append(query_result)
                    db.session.commit()
                    payload = submission.title
                    payload_text = submission.selftext
                    break
                else:
                    continue  

        r = requests.post("https://graph.facebook.com/v2.6/me/messages",
            params={"access_token": token},
            data=json.dumps({
                "recipient": {"id": recipient},
                "message": {"text": payload}
                #"message": {"text": text.decode('unicode_escape')}
            }),
            headers={'Content-type': 'application/json'})

        r = requests.post("https://graph.facebook.com/v2.6/me/messages",
            params={"access_token": token},
            data=json.dumps({
                "recipient": {"id": recipient},
                "message": {"text": payload_text,
                            "quick_replies":quick_replies_list}
                #"message": {"text": text.decode('unicode_escape')}
            }),
            headers={'Content-type': 'application/json'})
        
    else:
        payload = "http://imgur.com/WeyNGtQ.jpg"
        for submission in reddit.subreddit(subreddit_name).hot(limit=None):
            if ((submission.link_flair_css_class == 'image') or 
                ((submission.is_self != True) and 
                    ((".jpg" in submission.url) or 
                        (".png" in submission.url)))):
                query_result = (
                    Posts.query
                    .filter(Posts.name == submission.id).first()
                )
                if query_result is None:
                    myPost = Posts(submission.id, submission.url)
                    myUser.posts.append(myPost)
                    db.session.commit()
                    payload = submission.url
                    break
                elif myUser not in query_result.users:
                    myUser.posts.append(query_result)
                    db.session.commit()
                    payload = submission.url
                    break
                else:
                    continue

        print("Payload: ", payload)

        r = requests.post("https://graph.facebook.com/v2.6/me/messages",
            params={"access_token": token},
            data=json.dumps({
                "recipient": {"id": recipient},
                "message": {"attachment": {
                              "type": "image",
                              "payload": {
                                "url": payload
                              }},
                              "quick_replies":quick_replies_list}
                #"message": {"text": text.decode('unicode_escape')}
            }),
            headers={'Content-type': 'application/json'})

    if r.status_code != requests.codes.ok:
        print(r.text)

def get_or_create(session, model, **kwargs):
    instance = session.query(model).filter_by(**kwargs).first()
    if instance:
        return instance
    else:
        instance = model(**kwargs)
        session.add(instance)
        session.commit()
        return instance

relationship_table=db.Table('relationship_table',                            
    db.Column('user_id', db.Integer,db.ForeignKey('users.id'), nullable=False),
    db.Column('post_id',db.Integer,db.ForeignKey('posts.id'),nullable=False),
    db.PrimaryKeyConstraint('user_id', 'post_id') )
 
class Users(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(255),nullable=False)
    posts = db.relationship('Posts', secondary=relationship_table, backref='users' )  

    def __init__(self, name=None):
        self.name = name
 
class Posts(db.Model):
    id=db.Column(db.Integer, primary_key=True)
    name=db.Column(db.String, unique=True, nullable=False)
    url=db.Column(db.String, nullable=False)

    def __init__(self, name=None, url=None):
        self.name = name
        self.url = url

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

Note that there is an important change to the app.py file: instead of hardcoding the configuration, we are making use of the environment variables.

Also, we need to add flask-SQLAlchemy and Postgresql drivers to the requirements.txt file. Install both of these by running the following commands in the terminal:

pip install flask_sqlalchemy
pip install psycopg2-binary

Now, run pip freeze > requirements.txt. This will update the requirements.txt file. Your requirements.txt file should look something like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
click==7.1.2
Flask==1.1.2
gunicorn==20.0.4
itsdangerous==1.1.0
Jinja2==3.0.0a1
MarkupSafe==2.0.0a1
requests==2.24.0
Werkzeug==1.0.1
Flask-SQLAlchemy==2.4.4
psycopg2-binary==2.8.6
whitenoise==5.2.0
praw==7.1.0

We need to update the environment variables as well, so that the configuration for Reddit and Facebook are contained there. You can do that by running the following commands in the terminal:

heroku config:set REDDIT_ID=*********
heroku config:set REDDIT_SECRET=***********
heroku config:set FACEBOOK_TOKEN=***********

Note

replace ******* with your own configuration

Now let’s push everything to Heroku:

$ git add .
$ git commit -m "Updated the code with Reddit feature"
$ git push heroku master

One last step remains. You need to tell Heroku that you will be using a database. By default, Heroku does not provide a database for new apps. However, it is simple to set one up. Just execute the following command in the terminal:

$ heroku addons:create heroku-postgresql:hobby-dev

This will create a free hobby database, which is big enough for the project. Next, you need to initialize the database with the correct tables. In order to do this, you need to run the Python shell on our Heroku server:

$ heroku run python

In the Python shell, type the following commands:

>>> from app import db
>>> db.create_all()

If these commands work without a hiccup, congrats! The project is complete!

Before moving one, let’s discuss some interesting features of the code. We are making use of the quick replies feature of Facebook Messenger Bot API. This allows us to send some pre-formatted inputs which the user can quickly select (Fig. 6.15).

quick-replies

Fig. 6.15 Quick-replies in action

It’s easy to display these quick replies to the user. With every post request to the Facebook graph API, we send some additional data:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
quick_replies_list = [{
 "content_type":"text",
 "title":"Meme",
 "payload":"meme",
},
{
 "content_type":"text",
 "title":"Motivation",
 "payload":"motivation",
},
{
 "content_type":"text",
 "title":"Shower Thought",
 "payload":"Shower_Thought",
},
{
 "content_type":"text",
 "title":"Jokes",
 "payload":"Jokes",
}]

Another interesting feature is how we determine whether a post contains text, an image, or a video. In the GetMotivated subreddit, some images don’t have a .jpg or .png in their URL so we rely on:

submission.link_flair_css_class == 'image'

This way, we are able to select even those posts which do not have a known image extension in the URL.

You might have noticed this bit of code in the app.py file:

payload = "https://imgur.com/WeyNGtQ.jpg"

It makes sure that if no new posts are found for a particular user (every subreddit has a maximum number of “hot” posts), we still have something to return. Otherwise, we would get a variable undeclared error.

Create if the User doesn’t exist:

The following function checks whether a user with a particular name exists. If the user exists, the code selects that user from the db and returns it. In the case where the user doesn’t exist, the code creates the user and then returns that newly created user object:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
myUser = get_or_create(db.session, Users, name=recipient)
# ...

def get_or_create(session, model, **kwargs):
    instance = session.query(model).filter_by(**kwargs).first()
    if instance:
        return instance
    else:
        instance = model(**kwargs)
        session.add(instance)
        session.commit()
        return instance

The full code for this project is fairly long so I won’t be putting it in the book. You can look at the online repo for the final working code.

6.7 Troubleshoot

If you encounter any problems, you can try troubleshooting them using the following methods:

  • Check Heroku logs by running heroku logs -t

  • Make sure the correct environment variables are set by running heroku config

  • Test praw in the terminal first to make sure it is working as intended

If these tips don’t help, you can shoot me an email.

6.8 Next Steps

There are many different directions you can take with this project. Perhaps modifying the bot such that it sends you a motivational post each morning? You could work with cryptocurrency APIs and allow users to query the current exchange rate for a specific currency. Or something completely different! The options are endless!

I hope you enjoyed this chapter!