Обходим запрет messages API Вконтакте через Python

    Привет, Хабр. В своей предыдущей статье я рассказал о возможности доступа к методам раздела messages через документацию, для чего достаточно было лишь авторизоваться на сайте ВК. Многие тогда заявили, что это не является угрозой личным данным пользователей, а невозможность выкачать свои сообщения — недостаток платформы. Также в комментариях мне оставили ссылку на node.js библиотеку, которая может авторизовываться по логину/паролю и предоставлять доступ к API сообщений, притворяясь официальным приложением.

    Дисклеймер:


    Статья и весь написанный код создавались только в образовательных и исследовательских целях и никогда не использовались для незаконной деятельности. Автор не призывает вас повторять какие либо описанные здесь действия и не несёт за них никакой ответственности.


    Но не все люди знакомы c javascript и node.js, поэтому я и решил написать свою библиотеку на python, которым сейчас пользуются многие, позволяющую через «тестовые запросы» документации предоставить полный функционал messages API. Сразу прошу не злиться на меня в местах, где я буду повторять аспекты прошлого «выступления», потому что я хочу оформить эту статью в виде независимой документации.

    Как этим пользоваться?


    Сама библиотека находится в репозитории github-a (там же, в папке examples, находятся скрипт с примерами использования из этой статьи). Чтобы установить её на компьютер можно воспользоваться в терминале командой:
    pip install vk-messages

    Теперь мы можем импортировать из этого пакета главный класс и создать его экземпляр, указав логин, пароль, какой вид авторизации использует данный аккаунт, а также директорию, куда мы хотим сохранить куки-авторизации пользователя. Это необходимо, чтобы пользователям с двухфакторной авторизацией не пришлось при каждом запуске скрипта постоянном вводить код из сообщения.

    from vk_messages import MessagesAPI
    
    login, password = 'login', 'password'                                
    messages = MessagesAPI(login=login, password=password,
                                    two_factor=True, cookies_save_path='sessions/')
    

    И по сути, на этом всё. Теперь нам остаётся лишь открыть документацию и использовать интересующие нас методы. Хочу сразу отметить, что такой подход позволяет нам использовать практически любой метод из документации, даже не относящийся к разделу messages:

    history = messages.method('messages.getHistory', user_id='1234567', count=5)
    

    Также мы можем комбинировать данную библиотеку с другими, к примеру, через vk_api мы можем загрузить фотографии с компьютера (код для этого действия приводится у них в разделе examples), а через vk_messages прикрепить эти вложения к сообщению:

    from vk_messages.utils import get_random
    
    messages.method('messages.send', user_id=peer_id, message='Hello',
                attachment='photo123456_7891011', random_id=get_random())
    

    Из любопытства я реализовал классическую функцию, которая в заданной папке создаёт подпапки людей, с которыми общался человек, и пытается выкачать последние сообщения и абсолютные url-ы фотографий. К моему счастью, всё работало как часы, и лишних ошибок не было:

    from vk_messages.utils import fast_parser
    
    fast_parser(messages, path='parsing/',                    
           count_conv=10, messages_deep=400, photos_deep=100) 
    

    Теперь же я хочу перейти к одной из самых интересных частей этой библиотеки: имея куки авторизации, мы можем выполнять абсолютно любые действия. Приведу свой личный пример, когда для постов группы, в которой я состою, мне нужно было составить таблицу, состоящую из ID поста и его автора. Но в чём была загвоздка: официальный api возвращает только человека, который опубликовал статью. Воспользовавшись сниффером, я увидел, что при наведении на дату публикации поста эти данные подгружаются с сервера. И после этого я написал обёртку, которая позволяла отправлять сколько угодно подобных запросов, используя лишь ссылку поста и куки авторизации, чтобы получать авторов. В примере ниже останется лишь избавиться от ненужных тегов:

    def get_creators(post, cookies):
        group = -int(post.split('_')[0])
        response = requests.post('https://vk.com/al_page.php', cookies=cookies,
                   data=f"_ads_group_id={group}&act=post_author_data_tt&al=1&raw={post}")
    
        response_json = json.loads(response.text[4:])['payload'][1]
        return response_json[0]
    
    authors = get_creators(post='-12345_67890',
                        cookies=messages.get_cookies())
    print(authors)   
    

    Но что доказывает нам верхний кусок кода? Правильно, даже если ВК закроет тестовые запросы на своей документации, мы всегда сможем симулировать действия пользователя и получать нужную информацию. В качестве эксперимента я сделал небольшую функцию, которая через запросы «пролистывания» страницы может получать ссылки на фотографии без использования официального API.

    Код получился слишком большим, поэтому решил скрыть его
    def get_attachments(attachment_type, peer_id, count, offset, cookies_final):
        if attachment_type == 'photo':
            session = requests.Session()
            parsed = []
            
            response = session.post(f'https://vk.com/wkview.php',
                                data=f'act=show&al=1&dmcah=&loc=im&ref=&w=history{peer_id}_photo', cookies=cookies_final)
            
            response_json = json.loads(response.text[4:])
            
            try:
                last_offset = response_json['payload'][1][2]['offset']
                count_all = response_json['payload'][1][2]['count']
            except:
                last_offset = response_json['payload'][1][0]['offset']
                count_all = response_json['payload'][1][0]['count']
    
            while (len(parsed) < count + offset)  and (last_offset != count_all):
                response_json = json.loads(response.text[4:])
            
                try:
                    last_offset = response_json['payload'][1][2]['offset']
                except:
                    last_offset = response_json['payload'][1][0]['offset']
    
                photos_vk =  re.findall(r'<a href="/photo(\S*)?all=1"', response_json['payload'][1][1])
                mails =  re.findall(r"'(\S*)', {img: this ,", response_json['payload'][1][1])
    
                for photo, mail in zip(photos_vk, mails):
                    if len(parsed) < offset:
                        parsed.append(photo)
                        continue
                    
                    response = session.post(f'https://vk.com/al_photos.php', cookies=cookies_final,
                                data=f'act=show&al=1&al_ad=0&dmcah=&gid=0&list={mail}&module=im&photo={photo}')
                    
                    response_json = json.loads(response.text[4:])
                    photo_size = list(response_json['payload'][1][3][0].items())
                    photo_size.reverse()
                    
                    for i in range(len(photo_size)):
                        if 'attached_tags' in photo_size[i][0]:
                            photo_size = photo_size[:i]
                            break
                
                    parsed.append(photo_size) 
    
                response = session.post(f'https://vk.com/wkview.php', cookies=cookies_final,
                        data=f'act=show&al=1&offset={last_offset}&part=1&w=history{peer_id}_photo')
            
            return parsed[offset + 3 : offset + 3 + count]
    


    Выглядит ли это громоздко? Да. Работает ли это намного медленнее, чем официальный api? Да. Но если ВК отнимут последнюю возможность доступа к сообщениям, мы всегда сможем найти выход.

    Также отмечу, что я постарался добавить во все места библиотеки, где возможны ошибки, исключения с пояснениями, но если вы найдёте какие-то события, к которым не отображается пояснение, то прошу сообщить мне об этом.
    Думаю, это довольно очевидно, но я всё-таки должен вас предупредить, что нужно быть осторожнее с сохранением куки, так как они по умолчанию сохраняются в той же папке, откуда был запущен скрипт, так что не бросайте их где попало, ведь именно для этого есть возможность выбрать их место сохранения. Кончено, через некоторое время эти куки сами по себе станут невалидными, но до этого момента они могут являться серьёзной угрозой безопасности вашего аккаунта.

    Как это работает?


    Для тех, кто интересуется, что происходит под капотом данного скрипта, вкратце пройдусь по основным пунктам. При авторизации делаются простые request запросы, симулирующие вход пользователя, которые лишь слегка меняются в зависимости от вида авторизации, а после успешного входа куки сохраняются в pickle файл. При запросе к api через документацию, ко всем настраиваемым параметрам прибавляется «param_», то есть значение offset превратится в param_offset. Также в запросе передаётся hash-код, который содержится в атрибуте data-hash тэга кнопки «Выполнить». Насколько я заметил, это значение для каждого метода постоянно.

    Так же отмечу один важный момент: пароль отправляется в кодировке ANSI, где символы русского алфавита разделяются знаком "%", и данного кода достаточно для реализации такого декодирования. Это может стать проблемой для некоторых линукс пользователей, ведь, насколько я помню, эта кодировка не входит по умолчанию в python на этой операционной системе.

    self.password = str(password.encode('ANSI')).replace('\\x', '%')[2:-1]
    

    Также одной из проблем для меня стало странное поведение некоторых методов. К примеру, если я менял местами параметры, то скрипт мог вернуть ответ в 10 раз меньше запрашиваемого или вообще ничего не вернуть. Для решения этой проблемы я просто решил распарсивать и отправлять параметры в строгом порядке, как они указаны в документации. Возможно это простое совпадение, но после этого проблем подобного рода у меня не возникало:

    response = session.get(f'https://vk.com/dev/{name}', cookies=self.cookies_final)
    hash_data =  re.findall(r'data-hash="(\S*)"', response.text)
    
    soup = BeautifulSoup(response.text, features="html.parser")
    params = soup.findAll("div", {"class": "dev_const_param_name"})
    params = [cleanhtml(str(i)) for i in params]
    
    payload, checker = '', 0
    for param in params:
       if param in kwargs:
           checker += 1
           payload += '&{}={}'.format('param_' + \
       param, quote(str(kwargs[param]) if type(kwargs[param]) != bool else str(int(kwargs[param]))))
            
    if checker != len(kwargs):
       raise Exception_MessagesAPI('Some of the parametrs invalid', 'InvalidParameters')
    

    Итог


    Что же, для меня данная библиотека стала первым опытом написания «открытых» проектов, поэтому я прошу не судить её строго. Я просто хотел помочь людям, которые столкнутся с такой же проблемой как и я: ограничение messages API. Также я очень хочу поблагодарить знакомых, которые помогали мне с написанием этой статьи и тестированием кода.
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 12

      +1
      Скрипт не готовый до конца. Так что можно сделать в итоге? Выкачать все сообщения из лс?
        0

        Можно сделать абсолютно всё, что позволяет официальная документация на раздел messages.

        –1
        Громкое название. Обходим запрет messages API Вконтакте через… что, простите?!… Python???
        Вот они какие, рецепты для домохозяйки.

        1. Установить python;
        2. Установить пакет из pip;
        3. Включить в настройках браузера Developer tools;
        4. Открыть DevTools и найти нужную куку во вкладке Network;
        5. Создать файл, в нём создать экземпляр класса, вписать авторизационные данные;
        6. Открыть консоль, командой запустить скрипт;
        7. Извлекать профит.

        Отличное решение для среднестатистического пользователя Вконтакте.
          +1

          Извините что?


          1. Вы жалуетесь на то, что чтобы запустить пайтон скрипт, нужно перед этим установить интерпретатор Python. Здесь я вообще промолчу.
          2. Ээээм, написать команду: pip install <команда>, это такой сложный процесс?
            3,4. Где я сказал хоть словом о том, что нужно лишний раз сниффить трафик и создавать свои куки? Моя библиотека специально была сделана, чтобы помочь людям решить эту нудную проблемку.
          3. Ну, создать файл это вообще главная проблема. Для этого нужно посмотреть целый 10 часовой курс на ютубе. А создание экземпляра класса вообще можно скопировать из статьи. (Press F for ctrl-c and ctrl-v)
          4. Здесь тоже комментировать не хочу. Тем более у большинства людей в их среде разработки есть кнопка запуска скрипта.

          P.S.
          Вас послушать, так Вы вообще запросы к API пишете на листочке и отправляете почтой в офис ВК и ждёте ответ пару месяцев.

            0
            Вы намеков не понимаете… Скажу прямо. Вы неправильно выбрали средство реализации.

            Поясню. Задача, которую вы «решили», не из разряда математических или транслирующих. Проблема то сугубо-пользовательского характера. Всё по инструкции можно автоматизировать, а спрашивать куку браузера от пользователя — это как минимум некорректно. Всё это должно делаться автоматом.

            Вот есть бухгалтерша на связи, как раз хочет выкачать из группы рецепты, спрашивает как? Я то ей хоть и могу письмо написать, но общаемся сугубо по телефону, тем более не касается рабочих отношений для обсуждений по e-mail.

            То есть, сама задача то нормальная. Но решение мало-воспроизводимое.

            А так, на мой взгляд, я дельный совет даю. Если бы вы правильно выбрали средства реализации, то решение было бы в 2-3 строки кода. На 100% могу быть уверен, что меньше одного килобайта. А если пойти по официальному пути, — плюс килобайт файла манифеста. Надеюсь, этот намек вы поймёте.
              0
              Про какое другое «средство реализации» и про какой «официальный путь» вы мне говорите?
              Ограничение Messages API
              Стремясь предоставить наиболее высокий уровень защиты приватности и хранения личной переписки, с 15 февраля мы ограничиваем сторонним разработчикам доступ к API сообщений пользователей.
                0
                Я не про API. Я про userscript. А официальный способ — расширение для браузера firefox. Расширения Firefox работают как на десктопах, так и на мобильниках. — И там пользователь уже авторизован, то есть остаётся спрограммировать сами действия, и сделать какую-нибудь кнопочку для активации. Причем в руках будут все инструменты — и скролл сообщений, и автоподгрузка без каких либо запросов, — всё уже спрограммированно самим Вконтакте.

                Ну на python тоже неплохо, только код раз в 5 длинее и авторизация через колено… Но, справедливости ради, это хорошая разминка на навыки python.
                  0
                  Наверное, не до конца объяснил, нашел свободные 10 минут, чтобы разъяснить подробнее.

                  Что я имею в виду?

                  Userscript — это тоже самое что и расширение для браузера, но неофициальное. Поставляется в виде прямой ссылки на файл extension-name.user.js. Если стоит расширение, например, Greasemonkey (самый первый) или Violentmonkey (самый простой) или проприетарный Tampermonkey (самый продвинутый), то оно подхватит ссылку и скачает и сохранит у себя автоматом. Расширение просто запускает нужный JavaScript, когда браузер переходит на страничку, URL которой удовлетворяет регулярному выражению, написанному в заголовках файла userscript.

                  Что из себя представляет скрипт?

                  По поводу трёх строчек я конечно перегнул.

                  Будем считать что мы находимся на странице сообщений.

                  Нам необходимо:
                  1. проскролить тег с сообщениями;
                  2. если появился прелоадер, значит конец ещё не достигнут, продолжаем цикл, в противном случае выдаём результат;
                  3. через setTimeout() или setInterval() периодами по 100-200мс ожидаем пока не погаснет прелоадер;
                  4. когда прелоадер погас, значит сообщения загрузились, получаем их через document.querySelectorAll() и innerHTML;
                  5. переходим в начало цикла.


                  И там остаётся внедрить на страницу кнопочку активации.
                  Я бы её внедрил прямо на страницу сообщений где-нибудь рядом со смайликами.
                  const button = document.createElement('button');
                  button.innerHTML = 'получить все сообщения';
                  document.querySelector('селектор куда внедряем кнопку').appendChild(button);
                  button.addEventListener('click', () => { getMessages() } );
                  

                  И остаётся вопрос — куда выводить результаты работы. Тут можно хоть в консоль, хоть в window.alert(''), можно сделать отдельный div, а можно даже открыть новое окно через window.open('about:blank','_blank');, а можно даже в виде какого-нибудь xml-файла или json-файла для скачки через ссылку в base64.

                  Ну как бы на словах все просто. Сколько коротеньких строчек кода? Ну может 5-7 или даже 10. Правда?

                  Ладно. По поводу расширения. Кстати, есть конвертеры userscript -> extension. Код там тот же самый. Ничего мудрёнее нету. Архив надо будет ещё подписать своей подписью PGP или аналогами.

                  Mozilla премодерирует все новые расширения. Обычно на это уходит пара часов. Чем меньше кода в расширении, тем быстрее его апрувят. На сам Вконтакте им будет пофигу, то есть главное — чтобы не было вредоносного кода.

                  Последнее расширение, которое я добавлял где-то год назад, (запулил его где-то в обед по московскому времени) Mozilla апрувила минут через 40.

                  Как расширения, так и userscript прекрасно работают на мобильных телефонах, при условии что там запускается Firefox.

                  Надеюсь, эта информация поможет. Если будут, вопросы, обращайся, я захожу пару раз в день на Хабру, как будет свободная минута — отвечу.
            0

            А для публикации фото в профиль Instagram имеется какое-то решение?

              0
              У инстаграмма тоже есть свой api, но я им никогда не пользовался. Но скорее всего публикация фото реализована в этой библиотеке: Instagram-API-python
                0
                API там скудное, там нужно что-то подобное. То есть, надо авторизовываться как «обычный» браузер, с корректным User-agent и всеми признаками браузера.
                0
                Было, на php, недавно его забанили на гитхабе.

              Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

              Самое читаемое