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

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




Для написания расширения выбрал 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.

Комментарии (1)


  1. CCgames
    21.02.2016 05:34

    За счет возможности перезаписать функцию способ то сработает, но не проще ли обрывать запрос на al_im.php c act: 'a_mark_read' ?