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

в 17:53, , рубрики: api, backdoor, documentation, github, pip, python, python3, requests, библиотека python, Вконтакте API, Программирование, Социальные сети и сообщества

Привет. В своей предыдущей статье я рассказал о возможности доступа к методам раздела 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. Также я очень хочу поблагодарить знакомых, которые помогали мне с написанием этой статьи и тестированием кода.

Автор: Aragroth

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js