Пишем расширение для улучшения работы с ВКонтакте

от автора

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

  • Не оповещать собеседников о том, что вы набираете сообщение
  • Открывать полные сообщения, но не отмечать их прочитанными
  • Возможность прикрепления картинки к сообщению/комментарию по любой фразе для поиска



Для написания расширения выбрал kango фреймворк.

Изначально нужно выбрать правильный подход к работе с сайтом. Можно эмулировать действия пользователя, а можно внедриться в страницу и напрямую вызывать JavaScript-объекты для своих целей. Последний вариант является более правильным и простым. Все content-скрипты расширения запускаются в отдельной песочнице и разделяют только DOM со страницей, поэтому внутри них мы не сможем ничего делать.
Выход простой: script injection.

var script = document.createElement('script'); script.src = chrome.extension.getURL("vk_inject.js"); script.onload = function () {     this.parentNode.removeChild(this); }; (document.head || document.documentElement).appendChild(script); 

При этом не забудьте добавить vk_inject.js в файл-манифест:

«web_accessible_resources»: [«vk_inject.js»]

Теперь весь код внутри имеет полный доступ ко всем переменным вконтакте.

Не оповещать собеседников о том, что вы набираете сообщение

Можно долго обсуждать, зачем это нужно, но функция явно полезная. В таком подходе нужно продумать план-перехват кода обработчика нужной логики. Например, можно мониторить post/get запросы, или же просто поставить брейкпоинт на логическое событие. В нашем случае я открыл монитор XHR-запросов и начал набирать текст в личных сообщениях. Сразу же видим post-запрос вида:

По отправленным данным сразу ясно, что это то, что нам нужно. Достаточно теперь в исходниках найти код, который содержит ключевое слово: a_typing.

В файле im.js находим функцию в глобальном объекте IM:

IM = {   onMyTyping: function (peer) {     ...     var ts = vkNow();     if (cur.myTypingEvents[peer] && ts - cur.myTypingEvents[peer] < 5000) {       return;     }     ...     ajax.post('al_im.php', {act: 'a_typing', peer: peer, hash: tab.hash, gid: cur.gid});   } } 

Как видно, с интервалом в 5 секунд делается post-запрос на скрипт al_im.php. Функция не делает ничего важного, поэтому самый простой способ добиться желаемого результата: перезаписать метод onMyTyping и сделать его пустым. Возвращаемся к inject скрипту и добавляем:

if (typeof IM != "undefined") {     IM.onMyTyping = function (peer)     {         // Do nothing here, swallow ajax post about typing event     } } 

Теперь, печатая текст, собеседник никогда не увидит оповещения об этом.

Открывать полные сообщения, но не отмечать их прочитанными

Логично предложить, что с прочитанными сообщениями может сработать тот же метод. Наверняка где-то внутри есть код получения сообщения и отдельно пост-запрос для того, чтобы пометить его прочитанным (например, по наведению мышки). В этом случае я просто прошелся по списку методов внутри файла im.js, расставил брейкпоинты на методах, которые по названию как-то связаны с получением и обработкой входящих сообщений. После этого в дебаге проходил все шаги, пока не нашел в том же глобальном IM метод markPeer:

markPeer: function(peer) {     ...     ajax.post('al_im.php', {act: 'a_mark_read', peer: peer, ids: arr, hash: tab.hash, gid: cur.gid}, {onDone: function() { ... } } });     ... } 

Вопрос решается тем же путем. В проверку IM на существование дописываем:

IM.markPeer = function(peer) { } 


Как видите, сообщения на экране справа помечены как прочитанные, но у отправителя (слева) они все еще непрочитанные.

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

Возможность прикрепления картинки к сообщению/комментарию по любой фразе для поиска

Остался один из самых сложных пунктов в нашем списке. Очень часто хочется ответить в комментариях картинкой. Для этого нужно переходить на поиск в гугл картинки, открывать источник, копировать ссылку и вставлять в поле ввода комментария/сообщения. Идея была в том, чтобы по какому-то шаблону делать это все автоматически. Со временем пришел к такой форме:

.(котики)

После наборы этого шаблона расширение должно найти первую картинку по запросу «котики» в гугл картинках и приложить её к сообщению.

Первая из проблем — гугл закрыли api доступ к поиску по картинкам, а их универсальный search api engine стоит несколько долларов на тысячу запросов. Для разработки плагина это нерабочий вариант. План у меня такой:

На своем сервере держу phantomjs с загруженной страничкой гугла, на питоне пишу простейший веб-сервер, который принимает GET-запрос, передает его в selenium, который открывает поиск, вытаскивает первую картинку и получает прямую ссылку. Сервер же отвечает редиректом на эту прямую ссылку к картинке. Т.е. у меня есть сайт sample.com/image, а GET запрос на sample.com/image/котики даст редирект на первую картинку из поиска.

PhantomJS и Selenium

Одна из проблем последнего PhantomJS — утечки памяти. В таком случае просто невозможно долго держать в памяти этот процесс. Постепенно я нашел пару вариантов решения этой проблемы.

desired_cap = {     'phantomjs.page.settings.loadImages' : True,     'page.settings.clearMemoryCaches' : True, }  self.driver = webdriver.PhantomJS(desired_capabilities=desired_cap) self.driver.command_executor._commands['executePhantomScript'] = ('POST', '/session/$sessionId/phantom/execute') 

Не выключайте loadImages для PhantomJS! Иначе получите кучу мемликов. Чтобы исключить нагрузку из-за картинок и прочих медиа-файлов нужно выполнить такой код:

driver.execute('executePhantomScript', {'script': '''             var page = this;             page.onResourceRequested = function(request, networkRequest) {                 if (/\.(jpg|jpeg|png|gif|tif|tiff|mov|css)/i.test(request.url))                 {                     networkRequest.abort();                     return;                 }             }         ''', 'args': []}) 

Перезаписываем обработчик onResourceRequested и игнорируем любые запросы на медиа файлы.
Также время от времени нужно вызывать метод:

driver.execute('executePhantomScript', {'script': '''             var page = this;             page.clearMemoryCache();         ''', 'args': []}) 

Эти два метода позволяют более-менее долго работать с PhantomJS без особых утерь памяти.
Код selenium-класса, работающий с гугл картинками очень прост:

class GoogleImageSearch(SeleniumUtils):     def initSelenium(self):       driver = self.driver        # Initial load       driver.get('https://images.google.com')       driver.find_element_by_css_selector('input[type="text"]').send_keys("Max Frai")       driver.find_element_by_css_selector('button').click()      def findImage(self, query):         query = urllib.unquote(query).decode('utf8')         driver = self.driver          resultUrl = ''         try:           inputHandle = driver.find_element_by_css_selector('input[type="text"]')           inputHandle.clear()           inputHandle.send_keys(query)            driver.execute_script("""             var element = document.querySelector("div#center_col");             if (element) element.parentNode.removeChild(element);           """)            driver.find_element_by_css_selector('button').click()           while True:             time.sleep(0.25)             try:               driver.find_element_by_css_selector('a[href*="imgres"]').click()               resultUrl = driver.find_elements_by_css_selector('a.irc_but[href]')[1].get_attribute('href')               break             except:               continue          except:           pass 

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

Вот так выглядит сервер:

class SimpleServer(BaseHTTPRequestHandler):     def do_GET(self):       try:         if self.path.startswith('/q='):           resultImage = searchHandle.findImage(self.path[3:])            self.send_response(301)           self.send_header('Location', resultImage)           self.end_headers()        except Exception as e:             pass  if __name__ == "__main__":     PORT = 8042     httpd = SocketServer.TCPServer(("", PORT), SimpleServer)     httpd.serve_forever() 

Сервер обрабатывает только GET-запросы, смотрит на querystring, которая должна содержать параметр q и дальше ключевую фразу для поиска. Дальше выдает 301й код редиректа на ссылку картинки.

Все прекрасно работает, но появилась проблема в работе с социальной сетью вконтакте: там не поддерживается вставка картинок по ссылке с ip-адресом, а у меня был только один домен в наличии, который привязан к другому сайту. Проблема решилась модулем Proxy_mod для Apache.

Для этого домена в sites-available дописываем:

ProxyRequests Off
<Proxy *>
Order deny,allow
Allow from all

ProxyPass /image/ localhost:8042/q=
ProxyPassReverse /image/ localhost:8042/q=

Теперь любые запросы на image путь будут направлены на наш питон-сервер, который висит на 8042 порте.

Добавление картинок в сообщения

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

Здесь нам опять понадобится взаимодействовать с переменными вконтакте. Всего бывает три типа поля ввода: личные сообщения, поле ввода для новой публикации, поле ввода комментария.
Каждый из этих типов содержит медиа-объект с методом checkURL, который на входу получает прямую ссылку на внешнюю картинку и автоматически прикрепляет её к сообщению (перезаливая при этом на свои сервера).

Обычное поле ввода для новой публикации это textarea. Чтобы получить медиа объект нужно обратиться к глобальной переменной cur:

cur.wallAddMedia 

Поле ввода комментария это уже div с content editable. Получение media объекта выглядит так:

var composer = data(textArea, 'composer'); if (composer) elementHandle = composer.addMedia; 

Все это можно увидеть через дебаггер. Поле ввода личных сообщений тоже div, но получить объект можно так:

cur.imMedia 

До того, как я написал код inject в среду VK, добавился того же результата через вставку текста ссылки на картинку в поле ввода, дальше все само распознавалось и подтягивалось.

Для простоты проверка, написал плагин и выложил в web store.

ссылка на оригинал статьи https://habrahabr.ru/post/277651/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *