Всем привет, в этой статье хочу описать этапы разработки расширения для хрома, которое немного улучшит возможности веб-версии вконтакте. Я себе поставил такие цели:
Для написания расширения выбрал kango фреймворк.
Изначально нужно выбрать правильный подход к работе с сайтом. Можно эмулировать действия пользователя, а можно внедриться в страницу и напрямую вызывать JavaScript-объекты для своих целей. Последний вариант является более правильным и простым. Все content-скрипты расширения запускаются в отдельной песочнице и разделяют только DOM со страницей, поэтому внутри них мы не сможем ничего делать.
Выход простой: script injection.
При этом не забудьте добавить vk_inject.js в файл-манифест:
Теперь весь код внутри имеет полный доступ ко всем переменным вконтакте.
Можно долго обсуждать, зачем это нужно, но функция явно полезная. В таком подходе нужно продумать план-перехват кода обработчика нужной логики. Например, можно мониторить post/get запросы, или же просто поставить брейкпоинт на логическое событие. В нашем случае я открыл монитор XHR-запросов и начал набирать текст в личных сообщениях. Сразу же видим post-запрос вида:
По отправленным данным сразу ясно, что это то, что нам нужно. Достаточно теперь в исходниках найти код, который содержит ключевое слово: a_typing.
В файле im.js находим функцию в глобальном объекте IM:
Как видно, с интервалом в 5 секунд делается post-запрос на скрипт al_im.php. Функция не делает ничего важного, поэтому самый простой способ добиться желаемого результата: перезаписать метод onMyTyping и сделать его пустым. Возвращаемся к inject скрипту и добавляем:
Теперь, печатая текст, собеседник никогда не увидит оповещения об этом.
Логично предложить, что с прочитанными сообщениями может сработать тот же метод. Наверняка где-то внутри есть код получения сообщения и отдельно пост-запрос для того, чтобы пометить его прочитанным (например, по наведению мышки). В этом случае я просто прошелся по списку методов внутри файла im.js, расставил брейкпоинты на методах, которые по названию как-то связаны с получением и обработкой входящих сообщений. После этого в дебаге проходил все шаги, пока не нашел в том же глобальном IM метод markPeer:
Вопрос решается тем же путем. В проверку IM на существование дописываем:
Как видите, сообщения на экране справа помечены как прочитанные, но у отправителя (слева) они все еще непрочитанные.
Минусом такой реализации будет то, что код будет постоянно пытаться отправить пометку, что сообщения прочитаны. Поэтому желательно где-то в интерфейс добавить переключатель вида: не отмечать прочитанные сообщения. Теперь вы можете открывать любые сообщения и они останутся непрочитанными на сервере, пока вы не напишите ответ (в этом случае все непрочитанные сообщения автоматически помечаются как прочитанные).
Остался один из самых сложных пунктов в нашем списке. Очень часто хочется ответить в комментариях картинкой. Для этого нужно переходить на поиск в гугл картинки, открывать источник, копировать ссылку и вставлять в поле ввода комментария/сообщения. Идея была в том, чтобы по какому-то шаблону делать это все автоматически. Со временем пришел к такой форме:
После наборы этого шаблона расширение должно найти первую картинку по запросу «котики» в гугл картинках и приложить её к сообщению.
Первая из проблем — гугл закрыли api доступ к поиску по картинкам, а их универсальный search api engine стоит несколько долларов на тысячу запросов. Для разработки плагина это нерабочий вариант. План у меня такой:
На своем сервере держу phantomjs с загруженной страничкой гугла, на питоне пишу простейший веб-сервер, который принимает GET-запрос, передает его в selenium, который открывает поиск, вытаскивает первую картинку и получает прямую ссылку. Сервер же отвечает редиректом на эту прямую ссылку к картинке. Т.е. у меня есть сайт sample.com/image, а GET запрос на sample.com/image/котики даст редирект на первую картинку из поиска.
Одна из проблем последнего PhantomJS — утечки памяти. В таком случае просто невозможно долго держать в памяти этот процесс. Постепенно я нашел пару вариантов решения этой проблемы.
Не выключайте loadImages для PhantomJS! Иначе получите кучу мемликов. Чтобы исключить нагрузку из-за картинок и прочих медиа-файлов нужно выполнить такой код:
Перезаписываем обработчик onResourceRequested и игнорируем любые запросы на медиа файлы.
Также время от времени нужно вызывать метод:
Эти два метода позволяют более-менее долго работать с PhantomJS без особых утерь памяти.
Код selenium-класса, работающий с гугл картинками очень прост:
Первый раз загружаем поиск по картинкам и вводим любую фразу, чтобы получить расширенный интерфейс, а дальше просто меняем текст в поле ввода и нажимаем кнопку поиска. Перед поиском удаляем предыдущие картинки. Это нужно потому что интерфейс строится аяксом динамически, мы не знаем время по таймингу, которое нужно подождать. Для этого и нужен бесконечный скрипт, который пытается найти первую картинку.
Вот так выглядит сервер:
Сервер обрабатывает только GET-запросы, смотрит на querystring, которая должна содержать параметр q и дальше ключевую фразу для поиска. Дальше выдает 301й код редиректа на ссылку картинки.
Все прекрасно работает, но появилась проблема в работе с социальной сетью вконтакте: там не поддерживается вставка картинок по ссылке с ip-адресом, а у меня был только один домен в наличии, который привязан к другому сайту. Проблема решилась модулем Proxy_mod для Apache.
Для этого домена в sites-available дописываем:
ProxyPass /image/ localhost:8042/q=
ProxyPassReverse /image/ localhost:8042/q=
Теперь любые запросы на image путь будут направлены на наш питон-сервер, который висит на 8042 порте.
Теперь остается только распознавать шаблонную фразу поиска картинок и прикреплять их к сообщению. Не буду рассказывать о том, как слушать события нажатия клавиш, разбор текста с помощью регулярных выражений. Сразу перейду к коду прикрепления картинки.
Здесь нам опять понадобится взаимодействовать с переменными вконтакте. Всего бывает три типа поля ввода: личные сообщения, поле ввода для новой публикации, поле ввода комментария.
Каждый из этих типов содержит медиа-объект с методом checkURL, который на входу получает прямую ссылку на внешнюю картинку и автоматически прикрепляет её к сообщению (перезаливая при этом на свои сервера).
Обычное поле ввода для новой публикации это textarea. Чтобы получить медиа объект нужно обратиться к глобальной переменной cur:
Поле ввода комментария это уже div с content editable. Получение media объекта выглядит так:
Все это можно увидеть через дебаггер. Поле ввода личных сообщений тоже div, но получить объект можно так:
До того, как я написал код inject в среду VK, добавился того же результата через вставку текста ссылки на картинку в поле ввода, дальше все само распознавалось и подтягивалось.
Для простоты проверка, написал плагин и выложил в web store.
- Не оповещать собеседников о том, что вы набираете сообщение
- Открывать полные сообщения, но не отмечать их прочитанными
- Возможность прикрепления картинки к сообщению/комментарию по любой фразе для поиска
Для написания расширения выбрал 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.
CCgames
За счет возможности перезаписать функцию способ то сработает, но не проще ли обрывать запрос на al_im.php c act: 'a_mark_read' ?