Просим Вконтакте напомнить про пельмешки

    Сегодня меня вновь попросили напомнить человеку о важной вещи в определённое время. Но что делать, если я и про свои-то дела забываю постоянно, а уж тем более про дела кого-то ещё? И тут мне снова помог мой любимый 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 Вконтакте можно взять тут. Теперь работает без использования фэйкового аккаунта. Это несомненный плюс.
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 19

      +25
      Когда-то давно уже писали на эту тему:
      xxx: сейчас в гостях у подруги сижу
      xxx: наконец-то понял её идиотские статусы в контакте. она, оказывается, готовит с их помощью
      xxx: маша иванова «закинула пельмени в кипящую воду. снять через 10 минут», обновлено 7 минут назад…

      bash.org
        +14
        Позвольте, уважаемый, но как же API? Такие вещи даже регулярками делать некрасиво, я уж не говорю про string.split для парсинга html.
          +3
          Я согласен, через апи изящнее. Но писать через него немножечко дольше. Авторизация сложнее, доступ к диалогам не очень удобный(лично мне). Требовалось быстрое работающее решение. Не промышленное. Если будет время, приведу сиё в православный вид.
            +3
            У нас с товарищем в универе была такая шутка: «Да да, когда у меня будет много времени и денег, я обязательно <сделаю что-то>» Обычно бывает или то или другое, или даже вообще ничего из этого списка, поэтому сказать это — было тем же самым, что сказать «я никогда это не сделаю».

            Времени никогда нет, а API vk.com достаточно интересная и мощная штука, и я бы рекомендовал посвятить его изучению пару вечеров. Во время чтения документации у меня например возникло несколько интересных идей для мэшапов.
              –2
              API для не desktop приложений изучено вдоль и поперёк по работе(и нет, оно мне не нравится. Много очень неудобных вещей вроде постинга картинок через wall.post на стену). Desktop трогать нужды особой не было, поэтому мельком знаком, но мне всёравно не нравится их авторизация с, как там написано, типами дизайна страницы авторизации. Всёравно всё сводится к сабмиту формы в общем. А messages.send точно так же не проще, чем просто сабмит формочки в диалоге. Единственный плюс был бы в получении диалогов, но ради этого все остальные получения сессий и прочие игры в бирюлики делать не хотелось.
                +2
                Всё же время нашлось именно сегодня. Версию скрипта, работающего через API, добавил в статью.
                +2
                Ну не сказал бы что авторизация сложная… Я, например, написал на PHP авторизацию в 6 строчек. Да и к тому-же API работает в разы быстрее, чем авторизация через сам сайт.

                На днях Сделал 2 скрипта, которые парсят музыку из VK.

                Первый авторизуется на сайте VK через CURL. Проходя несколько стадий подтверждения личности я смог попасть на страницу поиска треков и пропарсить содержимое страницы. На это дело уходит от 300 до 500 мс. (340 строчек кода с возможностью вывода в 4 форматах для удобного парсинга. Кому интересно могу написать зачем я это сделал)

                Второй скрипт написан на API VK. Как я уже говорил там всего 6 строчек для авторизации. И, внимание, всего-лишь одна строчка для поиска треков. Причём в выводе может содержаться от 1 до 1000 результатов, когда в первом способе выводилось лишь от 1 до 50. На всё это дело уходит от 30 до 100мс. (111 строчек кода с теми-же 4 форматами вывода)

                Так что если вы гонитесь за скоростью выполнения скрипта и лаконичностью кода, то используйте API.
              –3
              Я одно время искал удобную кроссплатформенную программу-планировщик на год. Ну, знаете — хостинг оплатить, несколько доменов, через каждые три месяца с резервных симок надо что-то отправлять дабы их не отобрали и т.д. и т.п. Но что ни пробовал — ничего не нравилось. Уже была идея тоже самому что-нибудь придумать, но потом поменялась работа и как-то не до этого стало. А прочитав этот пост вспомнил вот — надо бы эту идею все как-то добить или выбрать имеющееся решение — может за год что появилось изящное, а может быть и предложенное решение попробовать: будет знакомство с python :)
                0
                Делаю такой сервис, если нужно уведомлю о запуске. Работать будет с ВК.
                  0
                  Не, мне нужна исключительно приватность и автономность. Светиться, да еще и в соцсетях это не мой подход :)
                  0
                  Может быть кто пользуется какими-то автономными планировщиками? Интересно было бы услышать.
                  0
                  А идея то очень хорошая!
                  Вконтакте хорошо сделали систему уведомлений, а т.к. вконтакт сейчас авторизирован и на компе (дома и на работе), телефоне, да еще и с browser notifications — долго быть действительно удобно.
                  • UFO just landed and posted this here
                      +2
                      А Google Now чем не устроил с его календарем. В последней версии там и напоминалки можно забить легко прямо из клиента. Да даже голосом можно сказать — типа «Напомнить завтра вечером купить батарейки». Единственный минус то что не у всех андроид. Да и то лечится легко. Забиваешь событие с напоминанием СМС или по почте.
                        –3
                        Во первых: кто пользуется Google+? (Этого уже достаточно)
                        Во вторых: автора иногда просят напомнить о каких-то делах, а это значит что эта напоминалка не только для себя, но и для остальных тоже.
                          +1
                          Ну я допустим не пользуюсь вконтактиком. Есть там акк, но заглядываю раз в неделю максимум. Завести почту на гугле с левой инфой и подцепить календарь, даже без Google+ я думаю намного проще чем использовать непонятно какой костыль к вконтакту. А вобше дело ваше. Програмисты любят велосипеды. Я вкурсе.
                            +2
                            Вы можете не пользоваться Google+, это необязательно для того, чтобы иметь доступ к этому функционалу — если у вас есть гугловский аккаунт, то у вас есть и календарь с кучей вариантов уведомлений о событиях (в том числе смс и письмо в почту, не говоря уж об уведомлении в Google Now).
                              +2
                              Да, календарь есть у любого мало-мальски крупного сервиса. Яндекс свой имеет.
                              Можно пользоваться даже оффлайн (!) календарём, если не нужна синхронизация между устройствами. Но нет, не хотим этого знать, не нужен календарь, будем просить какого-то знакомого что-то нам напомнить.
                                0
                                Согласен, просто в контексте треда речь шла именно о гугловских сервисах.

                        Only users with full accounts can post comments. Log in, please.