Сегодня мы создадим бота для Facebook Messenger, который будет присылать нам свежие мемы, мотивационные сообщения и шутки. В этой статье есть большая часть информации, которую нужно знать для создания своего бота.
Вот так будет выглядеть финальная версия нашего приложения:
Технический стек
Нам понадобятся:
Flask — фреймворк для разработки бэкенда. Поскольку он легкий, это позволит нам сосредоточиться на логике, а не на структуре папок;
Heroku — хостинг для бесплатного размещения нашего кода;
Reddit — как источник данных, потому что новые сообщения там появляются каждую минуту.
Создание приложения Reddit
Мы будем использовать Facebook, Heroku и Reddit. Во-первых, убедитесь, что у вас есть учетная запись в каждом из этих сервисов. Затем нужно создать приложение Reddit:
Нажмите кнопку «create an app…» и следуйте инструкциям на экране:
Два нижних поля не будут использоваться, поэтому оставьте их пустыми. В поле с описанием приложения лучше написать что-то связанное с проектом. Когда приложение начнёт делать много запросов, представители Reddit могут проверить, для чего оно.
Теперь, когда ваше приложение создано, вам нужно сохранить client_id и client_secret в безопасном месте:
Первая часть нашего проекта выполнена. Теперь нам нужно настроить базу для нашего приложения Heroku.
Создание приложения на Heroku
Перейдите на сайт Heroku и создайте новое приложение:
На следующей странице дайте своему приложению уникальное имя. Нажмите «Heroku CLI» и загрузите последнюю версию интерфейса командной строки Heroku для вашей операционной системы. Следуйте инструкциям на экране и вернитесь, как только интерфейс будет загружен.
from flask import Flask, request
import json
import requests
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 = ''
@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/v2.6/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__':
app.run()
Мы будем редактировать файл в соответствии с нашими потребностями. Бот Facebook будет работать следующим образом:
Facebook отправляет запрос на наш сервер всякий раз, когда пользователь пишет сообщение на нашей странице Facebook.
Мы отвечаем на запрос Facebook и сохраняем идентификатор пользователя и сообщение, которое было отправлено на нашу страницу.
Мы отвечаем на сообщение пользователя через Graph API, используя сохранённый идентификатор пользователя и идентификатор сообщения.
Подробный разбор приведенного выше кода доступен на веб-сайте Константиноса Цапраилиса. В этом посте я сосредоточусь главным образом на интеграции Reddit и использовании базы данных Postgres на Heroku.
Прежде чем двигаться дальше, разместим вышеприведённый код Python на Heroku. Для этого вам нужно создать локальный репозиторий Git. Выполните следующие шаги:
$ messenger-bot
$ cd messenger-bot
$ touch requirements.txt app.py Procfile
Выполните вышеприведенные команды в терминале и поместите вышеуказанный код Python в файл app.py. Потом поместите в Procfile следующее:
web: gunicorn app:app
Теперь мы должны сообщить Heroku, какие библиотеки Python будет использовать наше приложение. Эти библиотеки должны быть перечислены в файле requirements.txt:
Сохраните адрес из строки «remote: …» — это адрес вашего Heroku-приложения. Он понадобится нам на следующем этапе.
Создание приложения Facebook
Для создания приложения нам нужна его страница на Facebook. Таково требование Facebook — у каждого приложения должна быть страница.
Теперь нам нужно зарегистрировать новое приложение. Перейдите на страницу создания приложения и следуйте инструкциям ниже:
Затем перейдите в свой файл app.py и замените PAT в строке 9 на Page Access Token, который мы сохранили выше.
Cохраните все и отправьте код в Heroku.
$ git commit -am "Added in the PAT"
$ git push heroku master
Теперь, если вы перейдете на страницу Facebook и отправите сообщение на эту страницу, вы получите своё собственное сообщение в ответ. Это подтвердит, что мы всё сделали верно. Если сообщение не пришло, проверьте логи Heroku, которые дадут вам некоторое представление об ошибке. Вы можете получить доступ к логам следующим образом:
$ heroku logs -t -a
Примечание: бот будет отвечать только на ваши сообщения, потому что он ещё не одобрен Facebook. Однако вы можете добавить нескольких тестировщиков. Для этого следуйте указаниям на странице разработчика.
Получение данных с Reddit
Мы будем использовать данные из следующих источников:
Установим praw — Python-библиотеку сайта Reddit. Это легко сделать, введя следующую команду в терминале:
$ pip install praw
Теперь протестируем некоторые прелести Reddit в оболочке Python. В документации чётко показано, как получить доступ к Reddit и его сообществам. Сейчас самое подходящее время, чтобы использовать client_id и client_secret, которые мы создали в первой части статьи.
$ python
Python 2.7.13 (default, Dec 17 2016, 23:03:43)
[GCC 4.2.1 Compatible Apple LLVM 8.0.0 (clang-800.0.42.1)] 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.'
Не забудьте добавить в свой собственный client_id и client_secret вместо ****.
В приложении используется limit = None. Это позволит получать как можно больше сообщений. Изображения мы будем использовать только из GetMotivated и Memes, а текстовые сообщения только из Jokes и ShowerThoughts.
Теперь, когда мы знаем, как получить доступ к Reddit, используя библиотеку Python, мы можем продолжить и интегрировать его в app.py.
Добавим несколько дополнительных библиотек в наш файл requirements.txt, чтобы он выглядел примерно так:
Просто отправить пользователю изображение или текст, взятый из Reddit, было бы нетрудно. В функции send_message мы могли бы сделать что-то вроде этого:
import praw
...
def send_message(token, recipient, text):
"""Send the message text to recipient with id recipient.
"""
if "meme" in text.lower():
subreddit_name = "memes"
elif "shower" in text.lower():
subreddit_name = "Showerthoughts"
elif "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/v2.6/me/messages",
params={"access_token": token},
data=json.dumps({
"recipient": {"id": recipient},
"message": {"attachment": {
"type": "image",
"payload": {
"url": payload
}}
}),
headers={'Content-type': 'application/json'})
...
Но нам нужен идентификатор для каждого изображения или текста, отправляемого пользователю, чтобы не отправлять одну и ту же запись дважды. Чтобы решить эту проблему, мы будем использовать PostgresSQL и идентификаторы постов Reddit (у каждого поста на Reddit есть уникальный id).
Мы будем использовать отношение «многие-ко-многим». Создадим две таблицы:
пользователи;
посты.
Сначала определим их в нашем коде, а затем уже разберёмся, как это будет работать:
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)
Таким образом, в таблице будет два поля. Имя будет идентификатором, отправленным с запросом Facebook Messenger Webhook. Посты будут связаны с другой таблицей — «Посты». В таблице сообщений есть имя и URL-адрес. «Имя» будет заполнено идентификатором поста Reddit, а URL — адресом этого поста.
Итак, теперь наш окончательный код будет работать так:
Мы запрашиваем список сообщений из определенного сообщества Reddit:reddit.subreddit(subreddit_name).hot(limit=None)
Проверяем, был ли конкретный пост отправлен пользователю ранее.
Если сообщение уже было отправлено, мы будем продолжать запрашивать посты от Reddit, пока не найдем новую запись.
Если сообщение не было отправлено пользователю, мы отправляем сообщение и выходим из цикла.
Итоговый код app.py выглядит так:
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['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 "meme" in text.lower():
subreddit_name = "memes"
elif "shower" in text.lower():
subreddit_name = "Showerthoughts"
elif "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}
}),
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}
}),
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}
}),
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
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}
}),
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()
Отправьте его в Heroku:
$ git commit -am "Updated the code with Reddit feature"
$ git push heroku master
Осталось последнее. Нам нужно сказать Heroku, что мы будем использовать базу данных. Для этого введите в терминале следующую команду:
Так мы создадим базу данных, которой достаточно для нашего проекта. Теперь нам нужно инициализировать её правильными таблицами. Для этого мы сначала должны запустить оболочку Python на нашем сервере Heroku:
$ heroku run python
Затем в оболочке Python ввести следующие команды:
>>> from app import db
>>> db.create_all()
Наше приложение готово! Поздравляю!
Некоторые интересные особенности кода
Во-первых, в коде используется функция быстрых ответов интерфейса Facebook Messenger Bot API. Это позволяет нам отправлять некоторые предварительно отформатированные данные, которые пользователь может быстро выбрать. Они будут выглядеть примерно так:
С каждым запросом на публикацию в API Facebook мы отправляем дополнительные данные:
Еще одна интересная особенность кода заключается в том, как мы определяем, является ли сообщение текстом, изображением или видеозаписью. В сообществе GetMotivated некоторые изображения не имеют расширения «.jpg» или «.png» в их URL, поэтому мы полагаемся на:
submission.link_flair_css_class == 'image'
Вы могли заметить эту строку кода в файле app.py:
payload = "http://imgur.com/WeyNGtQ.jpg"
Она гарантирует, что если новые записи не будут найдены для конкретного пользователя (у каждого сообщества есть максимальное количество «горячих» сообщений), приложению будет, что отправить. В противном случае мы получим ошибку.
Следующая функция проверяет, существует ли пользователь с определенным именем или нет. Если он существует, она выбирает этого пользователя из базы данных и возвращает его. Если он не существует, она создает его, а затем возвращает вновь созданного пользователя:
Игры можно писать не только на плюсах и C#. Конечно, это будут не крупные проекты с мощным движком и огромным количеством героев и NPC. Рассказываем, как создаются игры на Python и какие есть геймдев-библиотеки.
Сегодня хочу поговорить про Django. Django — замечательный фреймворк. А еще крайне популярный, его часто осваивают новички и используют в своих проектах. На самом деле у Django есть и несколько минусов, а точнее нюансов, на которые важно обращать внимание и о которых важно знать, приступая к разработке.