Простой бот для приложений Вконтакте на Python.
Думаю все знают про приложения-игралки, со всяким рейтингом среди друзей, популярные среди обитателей вконтакта. В этом топике я расскажу, как можно быть в них на первом месте, при этом не задрачиваясь, и не отупляя свой мозг.
В большинстве случаев, приложения обмениваются с сервером информацией по HTTP, посылая информацию POST-запросом, в ответ получая XML. Наша задача состоит в том, чтобы эмитировать эти запросы. Нашим подопытным кроликом будет приложение «Тюгяга »
Какие инструменты нам потребуются:
- Python - Любимый всеми язык. Будем использовать только стандартную библиотеку.
- Charles - простой HTTP-прокси. Его мы будем использовать для перехвата и анализа запросов.
Итак, приступим:
Для начала запустим Charles. Разобраться в нём сможет даже ребёнок. Приложение советую запускать в Firefox’е. Попробуйте выполнить какое-нибудь действие в «Тюряге», например собрать сигареты, и посмотрите на последний запрос. В общем случае вы увидите что-то вроде этого:
POST /prison/universal.php?office HTTP/1.1
Host: 109.234.156.250
User-Agent: Mozilla/5.0 (Windows; U; Windows NT 6.1; ru; rv:1.9.2.15) Gecko/20110303 Firefox/3.6.15
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: ru-ru,ru;q=0.8,en-us;q=0.5,en;q=0.3
Accept-Encoding: gzip,deflate
Accept-Charset: windows-1251,utf-8;q=0.7,*;q=0.7
Keep-Alive: 115
Referer: xxxxx
Content-type: application/x-www-form-urlencoded
Content-length: 111
method=office&sig=4b8874255d3f868e4e9f4006e51635de&user=xxxxxxxxx&getidea=1&key=xxxxxxxxxxxxxx
Тут мы имеем URI, HTTP-заголовки, пустую строку, и данные, отправленные на сервер. Что же отправляется на сервер:
key - Ключ, необходимый для идентификации вас именно в этом приложении. Предоставляется контактом, для каждого приложения разный. По этому ключу невозможно взломать вашу страницу или узнать какие-нибудь ваши данные. Периодически меняется, кстати.
user - Ваш id.
sig - Сигнатура, генерируемая тюрягой. Честно говоря, я не разобрался для чего она служит и по какому принципу генерируется. Но без неё никак. Лично я просто отловил их несколько штук и рандомно подставляю для каждого запроса. :)
method - Сообщает серверу, что за действие вы совершили.
Отправляются ещё и дополнительные данные, такие как getidea - сообщает сколько сигарет вы собрали.
Переходим к питону:
#!/usr/bin/env python3 # t.py import urllib, time, http.client, random, threading from xml.dom import minidom
Импортируем необходимые модули из стандартной библиотеки
xml.dom.minidom нужен если вы собирайтесь обрабатывать XML-ответ.
sigs = ("21ddcf3ea352f24ebdfe65bce51258da","4b8874255d3f868e4e9f4006e51635de","8dbedd3e931c8fd4fc97fa0fa74a1a20", "2243effc5a45a32c67b06898fb0b9548","7f47c0b6e9e385b8234ca9dce375a0c5","bdab387b62bed654492b8c93670a537e", "f17687ea1a535d61124dadb30887d14c","938b2f649aec28319ae2433ead41dae5") h ={ "Host":"188.93.20.139", "User-Agent":"Mozilla/5.0 (Windows; U; Windows NT 6.1; ru; rv:1.9.2.15) Gecko/20110303 Firefox/3.6.15", "Accept":"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", "Accept-Language":"ru-ru,ru;q=0.8,en-us;q=0.5,en;q=0.3", "Accept-Charset":"windows-1251,utf-8;q=0.7,*;q=0.7", "Keep-Alive":"115", "Content-type":"application/x-www-form-urlencoded", } u = "Ваш_ID" k = "Ваш key, для идентификации в приложении"
Создаём кортеж из сигнатур, заголовки в форме словаря(лишние я удалил), и две переменные.
Def request(post): global h # используем в функции глобальную переменную h connection = http.client.HTTPConnection("109.234.155.196:80") # Создаём объект соединения connection.request("POST", "/prison/universal.php", body = post, headers = h) # отправляем запрос resp = connection.getresponse() # получаем ответ return resp.read() # возвращаем ответ из функции
Т.к. отправлять данные на придётся часто, создадим для этого отдельную функцию
Def take_sigareties(): global sigs, u, k # используем в функции глобальные переменные post = urllib.parse.urlencode({ "method" : "office", "getidea" : "5", "sig" : random.choice(sigs), "user" : u, "key" : k }) # с помощью метода urllib.parse.urlencode подготавливаем данные для отправки. данные скармливаем методу в виде словаря while True: # запускаем бесконечный цикл for _ in range(0,20): # запускае цикл for на 20 раз request(post) # с помощью функции, которую мы создали ранее, отправляем данные на сервер print("Собрал сиги") time.sleep(5*20*60+15) # усыпляем поток выполнения на 6015 секунд. т.е. следующая итерация for произойдёт через указанное время. time.sleep(8*60*60) # после того как собрали 20 раз сиги, усыпляем поток на 8 часиков, чтобы нас не спалили;)
Создаём функцию для сбора сигареток
По аналогии создадим функцию для сбора прибыли раз в 8 часов:
Def take_rewards(): global sigs, u, k post = urllib.parse.urlencode({ "method" : "getAllBuildingsRewards", "sig" : random.choice(sigs), "user" : u, "key" : k }) while True: request(post) print("Собрал прибыль") time.sleep(8*60*60+15)
Т.к. нам надо чтобы функции работали независимо друг от друга, запустим их в отдельных потоках:
T1 = threading.Thread(target = take_sigareties) # создаём объект потока, target"у подаётся созданная функция t1.deamon = True # это нужно для того, чтобы при закрытии основного потока, дочерний поток тоже закрывался t2 = threading.Thread(target = take_rewards) t2.deamon = True t1.start() # запускаем поток на выполнение t2.start()
На этом собственно всё, запускаем скрипт, и радуемся, что больше не надо заходить в приложение каждые 20 минут, чтобы собрать сигоретки.
По аналогии можно писать ботов для других приложений.
Вот полный код бота.
#!/usr/bin/env python3 # t.py import urllib, time, http.client, random, threading from xml.dom import minidom sigs = ("21ddcf3ea352f24ebdfe65bce51258da","4b8874255d3f868e4e9f4006e51635de","8dbedd3e931c8fd4fc97fa0fa74a1a20", "2243effc5a45a32c67b06898fb0b9548","7f47c0b6e9e385b8234ca9dce375a0c5","bdab387b62bed654492b8c93670a537e", "f17687ea1a535d61124dadb30887d14c","938b2f649aec28319ae2433ead41dae5") h ={ "Host":"188.93.20.139", "User-Agent":"Mozilla/5.0 (Windows; U; Windows NT 6.1; ru; rv:1.9.2.15) Gecko/20110303 Firefox/3.6.15", "Accept":"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", "Accept-Language":"ru-ru,ru;q=0.8,en-us;q=0.5,en;q=0.3", "Accept-Charset":"windows-1251,utf-8;q=0.7,*;q=0.7", "Keep-Alive":"115", "Content-type":"application/x-www-form-urlencoded", } u = "Ваш_ID" k = "Ваш key" def request(post): global h # используем в функции глобальную переменную h connection = http.client.HTTPConnection("109.234.155.196:80") # Создаём объект соединения connection.request("POST", "/prison/universal.php", body = post, headers = h) # отправляем запрос resp = connection.getresponse() # получаем ответ return resp.read() # возвращаем ответ из функции def take_rewards(): global sigs, u, k post = urllib.parse.urlencode({ "method" : "getAllBuildingsRewards", "sig" : random.choice(sigs), "user" : u, "key" : k }) while True: request(post) print("Собрал прибыль") time.sleep(8*60*60+15) def take_sigareties(): global sigs, u, k # используем в функции глобальные переменные post = urllib.parse.urlencode({ "method" : "office", "getidea" : "5", "sig" : random.choice(sigs), "user" : u, "key" : k }) # с помощью метода urllib.parse.urlencode подготавливаем данные для отправки. данные скармливаем методу в виде словаря while True: # запускаем бесконечный цикл for _ in range(0,20): # запускае цикл for на 20 раз request(post) # с помощью функции, которую мы создали ранее, отправляем данные на сервер print("Собрал сиги") time.sleep(5*20*60+15) # усыпляем поток выполнения на 6015 секунд. т.е. следующая итерация for произойдёт через указанное время. time.sleep(8*60*60) # после того как собрали 20 раз сиги, усыпляем поток на 8 часиков, чтобы нас не спалили;) def do_city_action(): global sigs, u, k while True: action_id = 1 while action_id < 8: for _ in range(0,5): post = urllib.parse.urlencode({ "method" : "doCityAction", "city" : "1", "action_id" : str(action_id), "action_type" : "3", "sig" : random.choice(sigs), "user" : u, "key" : k }) response = request(post) print("Выполнил задание") if int(minidom.parseString(str(response, "utf8")).getElementsByTagName("energy").firstChild.data) < 10: time.sleep(40*5*60) action_id +=1 def vote_friends_to_gym(): global sigs, u, k friends = () # кортеж друзей, которых вы хотите позвать в спортзал while True: for friend in friends: post = urllib.parse.urlencode({ "method" : "voteForFriend", "model_id" : "1", "sex" : "0", "friend_uid" : friend, "vote" : "5", "username" : "London Eyes", "sig" : random.choice(sigs), "user" : u, "key" : k }) request(post) print("Позвал " + friend + " в спортзал") time.sleep(24*60*60+15) def break_terpilas_faces(): global sigs, u, k terpilas = () # кортеж друзей, на которых вы хотите наехать while True: for terpila in terpilas: post = urllib.parse.urlencode({ "method" : "challengeToDuel", "enemy" : terpilas, "sig" : random.choice(sigs), "user" : u, "key" : k }) request(post) print("Наехал на " + terpila) time.sleep(24*60*60+15) def send_presents(): recipients = "" # чезез запятую id друзей, которым вы хотите заслать подогрев while True: post = urllib.parse.urlencode({ "method" : "sendPresent", "recipients" : recipients, "present_id" : "5", "sig" : random.choice(sigs), "user" : u, "key" : k }) request(post) print("Послал подогревы") time.sleep(24*60*60+15) t1 = threading.Thread(target = take_rewards) # создаём объект потока, target"у подаётся созданная функция t1.deamon = True # это нужно для того, чтобы при закрытии основного потока, дочерний поток тоже закрывался t2 = threading.Thread(target = take_sigareties) t2.deamon = True t3 = threading.Thread(target = do_city_action) t3.deamon = True t4 = threading.Thread(target = vote_friends_to_gym) t4.deamon = True t5 = threading.Thread(target = break_terpilas_faces) t5.deamon = True t6 = threading.Thread(target = send_presents) t6.deamon = True t1.start() # запускаем поток на выполнение t2.start() t3.start() t4.start() t5.start() t6.start()
Сегодня мы создадим бота для Facebook Messenger, который будет присылать нам свежие мемы, мотивационные сообщения и шутки. В этой статье есть большая часть информации, которую нужно знать для создания своего бота.
Вот так будет выглядеть финальная версия нашего приложения:
Технический стек
Нажмите кнопку «create an app…» и следуйте инструкциям на экране:
Два нижних поля не будут использоваться, поэтому оставьте их пустыми. В поле с описанием приложения лучше написать что-то связанное с проектом. К огда приложение начнёт делать много запросов, представители Reddit могут проверить, для чего оно.
Теперь, когда ваше приложение создано, вам нужно сохранить client_id и client_secret в безопасном месте:
Первая часть нашего проекта выполнена. Теперь нам нужно настроить базу для нашего приложения Heroku.
Создание приложения на Heroku
На следующей странице дайте своему приложению уникальное имя. Нажмите «Heroku CLI» и загрузите последнюю версию интерфейса командной строки Heroku для вашей операционной системы. Следуйте инструкциям на экране и вернитесь, как только интерфейс будет загружен.
Создание базового приложения Python
Приведенный ниже код взят с сайта Константиноса Цапраилиса .
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"]["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.
$ messenger-bot $ cd messenger-bot $ touch requirements.txt app.py Procfile
Выполните вышеприведенные команды в терминале и поместите вышеуказанный код Python в файл app.py . Потом поместите в Procfile следующее:
Web: gunicorn app:app
Теперь мы должны сообщить Heroku, какие библиотеки Python будет использовать наше приложение. Эти библиотеки должны быть перечислены в файле requirements.txt:
Click==6.6 Flask==0.11 gunicorn==19.6.0 itsdangerous==0.24 Jinja2==2.8 MarkupSafe==0.23 requests==2.10.0 Werkzeug==0.11.10
Выполните следующую команду в терминале. Вы получите такой результат:
$ ls Procfile app.py requirements.txt
Теперь мы создадим Git-репозиторий, который затем может быть помещен на серверы Heroku. Для его создания мы должны выполнить следующие шаги:
- Войти в Heroku.
- Создать новый репозиторий.
- Сохранить состояние проекта в репозиторий.
- Отправить его на Heroku.
Для этого введите следующие команды в терминал:
$ heroku login $ git init $ heroku git:remote -a $ git commit -am "Initial commit" $ git push heroku master ... remote: https://.herokuapp.com/ deployed to Heroku ... $ heroku config:set WEB_CONCURRENCY=3
Сохраните адрес из строки «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) 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" Hi, Stranger."
Не забудьте добавить в свой собственный client_id и client_secret вместо **** .
В приложении используется limit = None . Это позволит получать как можно больше сообщений. Изображения мы будем использовать только из GetMotivated и Memes, а текстовые сообщения только из Jokes и ShowerThoughts.
Теперь, когда мы знаем, как получить доступ к Reddit, используя библиотеку Python, мы можем продолжить и интегрировать его в app.py .
Добавим несколько дополнительных библиотек в наш файл requirements.txt , чтобы он выглядел примерно так:
$ cat requirements.txt click==6.6 Flask==0.11 gunicorn==19.6.0 itsdangerous==0.24 Jinja2==2.8 MarkupSafe==0.23 requests==2.10.0 Werkzeug==0.11.10 flask-sqlalchemy psycopg2 praw
Просто отправить пользователю изображение или текст, взятый из 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"]["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, что мы будем использовать базу данных. Для этого введите в терминале следующую команду:
$ heroku addons:create heroku-postgresql:hobby-dev --app
Так мы создадим базу данных, которой достаточно для нашего проекта. Теперь нам нужно инициализировать её правильными таблицами. Для этого мы сначала должны запустить оболочку Python на нашем сервере Heroku:
$ heroku run python
Затем в оболочке Python ввести следующие команды:
>>> from app import db >>> db.create_all()
Наше приложение готово! Поздравляю!
Некоторые интересные особенности кода
Во-первых, в коде используется функция быстрых ответов интерфейса Facebook Messenger Bot API. Это позволяет нам отправлять некоторые предварительно отформатированные данные, которые пользователь может быстро выбрать. Они будут выглядеть примерно так:
С каждым запросом на публикацию в API Facebook мы отправляем дополнительные данные:
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", }]
Еще одна интересная особенность кода заключается в том, как мы определяем, является ли сообщение текстом, изображением или видеозаписью. В сообществе GetMotivated некоторые изображения не имеют расширения «.jpg» или «.png» в их URL, поэтому мы полагаемся на:
Submission.link_flair_css_class == "image"
Вы могли заметить эту строку кода в файле app.py:
Payload = "http://imgur.com/WeyNGtQ.jpg"
Она гарантирует, что если новые записи не будут найдены для конкретного пользователя (у каждого сообщества есть максимальное количество «горячих» сообщений), приложению будет, что отправить. В противном случае мы получим ошибку.
Следующая функция проверяет, существует ли пользователь с определенным именем или нет. Если он существует, она выбирает этого пользователя из базы данных и возвращает его. Если он не существует, она создает его, а затем возвращает вновь созданного пользователя:
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
К сожалению, на данный момент нет хороших библиотек на Python2, для того, чтобы быстро создать чат-бота. Ниже я покажу, как легко можно написать примитивного чат бота для VK, используя API VK.
Статья написана для новичков, чтобы показать, что ничего сложного в написании ботов на Python нет.
Авторизация
В первом случае надо будет ввести логин и пароль. Во втором случае в группе надо включить "Сообщения сообщества" и создать ключ доступа к API:
import time import vk_api vk = vk_api.VkApi(login = "login", password = "password") #vk_api.VkApi(token = "a02d...e83fd") #Авторизоваться как сообщество vk.auth()
Отправка сообщений
Теперь напишем короткую функцию, которая отправляет сообщение выбранному человеку.
P.S. Сообщество может отправлять сообщения только ранее писавшим пользователям.
def write_msg(user_id, s): vk.method("messages.send", {"user_id":user_id,"message":s})
В vk.method мы можем вызывать любой метод из VK API и передавать параметры в виде словаря.
В данном случае мы вызываем метод messages.send и в качестве параметров передаем id пользователя и текст сообщения.
Прием сообщений
Отлично! Отправлять сообщения мы научились, осталось научиться их принимать. Для этого нам нужен метод messages.get .
Несколько параметров, на которые стоит обратить внимание:
1) out - если этот параметр равен 1, сервер вернет исходящие сообщения.
2) count - количество сообщений, которое необходимо получить.
3) time_offset - максимальное время, прошедшее с момента отправки сообщения до текущего момента в секундах.
4) last_message_id - идентификатор сообщения, полученного перед тем, которое нужно вернуть последним (при условии, что после него было получено не более count сообщений)
values = {"out": 0,"count": 100,"time_offset": 60} vk.method("messages.get", values)
В нашем случае этот метод вернет все полученные сообщения за последние 60 сек, если их конечно было меньше 100, а если больше, то последние 100.
В итоге мы получаем список items:
{u"count": 3441, u"items": [{u"body": u"\u041f\u0438\u0448\u0435\u043c \u0431\u043e\u0442\u0430 \u0434\u043b\u044f \u0432\u043a!", u"date": 1491934484, u"id": 7387, u"out": 0, u"read_state": 0, u"title": u" ... ", u"user_id": 23107592}, {u"body": u"\u041f\u0440\u0438\u0432\u0435\u0442 \u0425\u0430\u0431\u0440!", u"date": 1491934479, u"id": 7386, u"out": 0, u"read_state": 0, u"title": u" ... ", u"user_id": 23107592}]}
Если объяснять простыми словами, то items - это то, что можно выделить в диалоге.
Финальный аккорд, делаем вечный цикл, где на каждое сообщение будем отвечать "Привет, Хабр!".
while True: response = vk.method("messages.get", values) if response["items"]: values["last_message_id"] = response["items"]["id"] for item in response["items"]: write_msg(item,u"Привет, Хабр!") time.sleep(1)
Чат-бот готов.
P.S. Мы запоминаем параметр last_message_id, чтобы в следующий раз обрабатывать только новые сообщения.
Полный код
# -*- coding: utf-8 -*- import time import vk_api vk = vk_api.VkApi(login = "login", password = "password") #vk_api.VkApi(token = "a02d...e83fd") #Авторизоваться как сообщество vk.auth() values = {"out": 0,"count": 100,"time_offset": 60} def write_msg(user_id, s): vk.method("messages.send", {"user_id":user_id,"message":s}) while True: response = vk.method("messages.get", values) if response["items"]: values["last_message_id"] = response["items"]["id"] for item in response["items"]: write_msg(item,u"Привет, Хабр!") time.sleep(1)
Получилось 17 строк кода. Успехов!