Просим Вконтакте напомнить про пельмешки
- понедельник, 17 марта 2014 г. в 06:00:16
http://habrahabr.ru/post/216001/
Сегодня меня вновь попросили напомнить человеку о важной вещи в определённое время. Но что делать, если я и про свои-то дела забываю постоянно, а уж тем более про дела кого-то ещё? И тут мне снова помог мой любимый python.
Честно говоря, обычные программы-напоминалки, что в телефоне, что в компьютере, меня не устраивали из-за их ограниченности рамками устройства + они совершенно не решали задачу, когда нужно напомнить о чём-то, но не мне. Решение пришло как-то само-собой. А что, если напоминания будут приходить как сообщения вконтакте? Если я не на рабочем месте — телефон свибрирует своим пуш-ап уведомлением, а за компьютером всё ещё очевиднее. Цель — написать скрипт, который читает мои сообщения о напоминании и в заданное время напоминает кому нужно о том, что, собственно, требуется. Ну раз идея пришла, я приступаю к её реализации.
Внимание! В этой статье я покажу как сделать это без использования API Вконтакте. Но для тех, кому хочется посмотреть(или использовать), как это делается с помощью, собственно, API, я прикладываю версию скрипта, работающего через API. Ссылка на него в конце статьи.
Для начала научим наш скрипт логиниться в эту социальную сеть. Всё просто, используем стандартный mechanize.Browser()
br = mechanize.Browser() br.set_handle_equiv(True) br.set_handle_redirect(True) br.set_handle_robots(False) br.open('https://vk.com/') br.select_form(nr=0) br.form['email'] = name br.form['pass'] = password br.submit()
Вуаля! Мы зашли на свою страничку вконтакте. Теперь используем классную особенность вконтакта — возможность писать самому себе(кто не в курсе, как это делается — перейдите по ссылке vk.com/im?sel=id, где id — Ваш id в социальной сети. В моём случае это был 38591009).
Первым делом прочитаем эти сообщения. Для этого мне нужно узнать свой id(он есть в коде главной страницы, причём повторяется множество раз).
self_username = 'username' def get_self_page_id(br): br.open('https://vk.com/'+self_username) return br.response().read().split('<form action="/wall')[1].split('?')[0] def check_messages(br): br.open('https://vk.com/im?sel='+get_self_page_id(br)) response = br.response().read()
Мы видим последние 20 сообщений из тех, что сами же себе и отсылали. Нам этого достаточно. Каждое сообщение имеет свой уникальный(для пользователя) номер, нам это очень полезно. Дальше нужно с ними немного поиграть, чтобы разделить все сообщения, отделить текст от порядкового номера и научить скрипт понимать, какие сообщения новые, а какие уже не актуальны.
first_start = True msg_numbers = [] #номера сообщений. Глобальная переменная, будет хранить номера сообщений, прочитанных в предыдущей итерации. def play_with_messages(br, response): global first_start all_messages = response.split('class="messages bl_cont">')[1].split('<div id="mfoot"')[0].split('<a name="msg') all_numbers = [] global msg_numbers for msg in all_messages: if msg != all_messages[0]: msg_num = msg.split('">')[0] all_numbers.append(msg_num) if first_start: msg_numbers = all_numbers first_start = False new_numbers = set(all_numbers) - set(all_numbers).intersection(set(msg_numbers)) for num in new_numbers: reply_to_message(br, get_message_text(response, num)) #вызов функции ответа на сообщение. Опишу её позже. msg_numbers = all_numbers
Отлично. Теперь мы знаем какие сообщения поступили мне от меня недавно. Осталось их понять и сделать что-то в ответ. Займёмся сначала первой задачей:
def reply_to_message(br, message): if message.find('напомнить') == -1: print 'nothing' else: print 'I obey, my lord' ms_words = message.split(' ') user = 'self' time_s = datetime.datetime.now().strftime('%H:%M') day_s = str(datetime.date.today()) msg = 'something went wrong' times = message.split('|') if len(times) == 1: times = '1' else: times = int(times[1]) if ms_words[1] == 'в': user = 'self' time_s = ms_words[2] msg = message.split('текст ')[1].split('|')[0] elif ms_words[1] == 'день': user = 'self' time_s = ms_words[4] day_s = ms_words[2] msg = message.split('текст ')[1].split('|')[0] elif ms_words[2] == 'в': user = get_page_id(br, ms_words[1]) time_s = ms_words[3] msg = message.split('текст ')[1].split('|')[0] elif ms_words[2] == 'день': user = get_page_id(br, ms_words[1]) time_s = ms_words[5] day_s = ms_words[3] msg = message.split('текст ')[1].split('|')[0] let_it_do(user, time_s, day_s, msg, times) #вызов функции, которая знает, что делать с полученными из сообщения значениями.
Здесь я спличу полученные сообщения и заношу в переменные соответствующие значения. В общем, отвечаю на вопросы «кому напомнить?», «что напомнить?», «когда и сколько раз это сделать?». Синтаксис сообщения/команды выбрал не сложный: напомнить [кому] [дата] в [время] текст [текст сообщения]|[сколько раз]. Вот пример:«напомнить tenoclock в 14:10 текст Очередной тест | 4»
Так наш робот видит внутренний диалог
Для хранения заданий я выбрал базу данных sqlite3. Нагрузка у нас минимальная, разворачивается она совершенно без усилий. Теперь приступим к записи заданий в базу данных, по пути проверяя валидность даты и времени. Выглядит это вот так:
def valid_time(time_text): try: datetime.datetime.strptime(time_text, '%H:%M') return True except ValueError: send_message(br_fake, get_self_page_id(br), 'неверный формат времени') return False def valid_date(date_text): try: datetime.datetime.strptime(date_text, '%Y-%m-%d') return True except ValueError: send_message(br_fake, get_self_page_id(br), 'неверный формат даты') return False def let_it_do(user, time_s, day_s, message, times): if valid_time(time_s) and valid_date(day_s): c = conn.cursor() c.execute("INSERT INTO reminder (time, date, user, message, times) VALUES (?,?,?,?,?)",(time_s, day_s, user, message, str(times))) conn.commit()
Мы уже близки к финалу! Задания наш робот получил, себе их записал. По сути, осталось только их выполнить. Тут я столкнулся с небольшой трудностью. Скрипт постоянно читает мои сообщения ко мне, поэтому, если он будет отправлять их в этот-же диалог, то в непрочитанных у меня ничего висеть не будет. А это плохо. Проблема решилась заведением фэйкового аккаунта для этого случая. Теперь если скрипт напоминает мне о чём-то, он пишет со второго аккаунта, если же нужно напомнить кому-то другому, то он пишет от моего имени, дабы людей не пугать.
Собственно вот пара функций, которые отвечают за чтение из базы и отсылку сообщений:
def check_answers(): conn = sqlite3.connect('reminder.db') rows = get_rows(conn) for row in rows: print row[5] c = conn.cursor() if row[3] == 'self': pass send_message(br_fake, get_self_page_id(br), row[4].encode('utf-8')) else: send_message(br, row[3], row[4].encode('utf-8')) if row[5] == '1' or row[5] == 1: c.execute("DELETE FROM reminder WHERE id = ?;", str(row[0])) else: time_s = (datetime.datetime.now()+datetime.timedelta(seconds=60)).strftime('%H:%M') num = int(row[5]) - 1 c.execute("UPDATE reminder SET time = ?, times = ? WHERE id = ?",(time_s, str(num), row[0])) conn.commit() def send_message(br, id, message): br.open('https://vk.com/im?sel='+id) br.select_form(nr=0) br.form['message'] = message br.submit()
Ну и после отсылки сообщений скрипт удаляет запись из базы, если она не актуальна(если нужно повторить ещё сколько-то раз, то переносит время напоминания на минуту вперёд и уменьшает количество оставшихся отправок)
Так скрипт, который укладывается в 200 строк кода, решает проблему напоминаний себе и другим, используя социальную сеть вконтакте. Целиком его можно скачать отсюда. Если он вдруг кому-то нужен, то советую не собирать из кусков статьи, здесь только функциональные вещи. Некоторые вспомогательные штуки остались за кадром. Я запустил его на одном из своих vps. Пока, вроде как, удобно.
Робот указывает мне, что делать. В воскресенье! Дожили
После несложных модификаций сюда так-же можно включить любые другие функции управления системой, если скрипт запущен на удалённом компьютере. Поставить тот же торрент на скачивание, например. А так, в целом, можно реализовать веб-сервис, который будет заниматься напоминаниями для всех, кто попросит(фактически бота, как в, уже забытых сейчас, irc и icq) Но эти вещи уже не относятся к данной статье. Буду очень рад, если кому-то это было полезным.
Всем спасибо за внимание.
UPD: Не спешите разжигать факелы и точить вилы по поводу API. Я сознательно его не использую в этом скрипте из-за некоторых неудобных мест. А именно авторизации и работы с диалогами. Впрочем, в первых же комментариях я раскрыл эту тему. Цель этой статьи показать, как быстро и, практически, не применяя сторонней информации, кроме знания родного для разработчика языка, сделать жизнь несколько проще.
UPD #2: Тот же скрипт, но с использованием могучего API Вконтакте можно взять тут. Теперь работает без использования фэйкового аккаунта. Это несомненный плюс.