Всем привет, в этой статье хочу описать этапы разработки расширения для хрома, которое немного улучшит возможности веб-версии вконтакте. Я себе поставил такие цели:
- Не оповещать собеседников о том, что вы набираете сообщение
- Открывать полные сообщения, но не отмечать их прочитанными
- Возможность прикрепления картинки к сообщению/комментарию по любой фразе для поиска
Для написания расширения выбрал 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/
Добавить комментарий