Асинхронная работа с ВКонтакте

Асинхронная работа с ВКонтакте

У широко известной соцсети ВКонтакте есть чуть менее известный API, насчитывающий немалое количество методов, а также различные способы загрузки файлов для использования в методах - и даже long-polling для получения сообщений из мессенджера ВКонтакте. В этой статье я попытаюсь рассказать о том, как работать в ruby с этим API в асинхронном режиме.

Для этого напишем собственный web-based мессенджер, умеющий отправлять сообщения, принимать и помечать сообщения прочитанными. Большая часть приложения будет оперировать на клиенте: фронт-энд будет устанавливать постоянное соединение с бэк-эндом, отправлять ему запросы и получать ответы; а бэк-энд займется непосредственной работой с API.

Самый примечательный момент в таком приложении - асинхронность: бэк-энд должен постоянно ждать от ВКонтакте новых сообщений, и параллельно с этим обрабатывать данные, присылаемые фронт-эндом, после чего вызывать нужный метод API (например, фронт-энд сообщает, что 2 входящих сообщения прочитаны - необходимо вызвать API-метод messages.markAsRead).

Задача

Итак, попробуем описать схему работы всего приложения.

  • браузер устанавливает WebSocket-соединение с сервером
  • бэк-энд запрашивает у ВКонтакте список друзей и непрочитанных сообщений
  • получив друзей и сообщения, бэк-энд отправляет данные на фронт-энд, и происходит первоначальный рендеринг интерфейса
  • отправив данные в браузер, бэк-энд получает параметры long-polling и начинает опрашивать ВКонтакте в вечном цикле
  • в конце каждой удачной итерации бэк-энд отправляет полученные данные в веб-сокет
  • в браузере каждая полученная от сервера порция обновлений разбирается по типам и рендерится
  • когда пользователь открывает вкладку одного из своих друзей, фронт-энд запрашивает у бэк-энда последние сообщения от этого пользователя
  • бэк-энд запрашивает сообщения через API, получает и отправляет в веб-сокет, после чего фронт-энд рендерит их в нужной вкладке

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

Реализация Инструментарий

Существует определенное количество ruby-врапперов для ВКонтакте API, но из известных мне библиотек только vkontakte_api позволяет работать с API асинхронно, поэтому в данном приложении будем использовать его. Для организации асинхронной работы на бэк-энде возьмем eventmachine, избежим callback spaghetti с помощью em-synchrony, а общаться с фронт-эндом будем посредством WebSocket (для чего используем em-websocket).

Файловую иерархию организуем следующим образом: в lib/ положим ruby-код, в public/ отправится единственный нужный нам html-файл index.html , а в public/css/ , public/img/ и public/js/ соответственно стили, картинки и яваскрипт + кофескрипт.

Бэк-энд

Чтобы не усложнять себе жизнь, подключим все необходимые гемы через bundler (не забывая выполнить bundle install после этого):

Для упрощения локального запуска и деплоя приложения используем библиотеку foreman, которая позволит описать процесс запуска в Procfile . Основной рабочий скрипт будет находиться в lib/main.rb :

lib/main.rb нужен для обработки запросов, приходящих по WebSocket; тут мы конфигурируем VkontakteApi , создаем клиент API в глобальной переменной (т.к. для всех запросов будем использовать именно его) и делегируем основную работу классу Messenger (передавая ему параметром объект WebSocket-а, дабы тот смог отправлять данные на фронт-энд):

Long-polling в исполнении ВКонтакте работает следующим образом: приложение делает HTTP-запрос на определенный URL с определенными параметрами (и URL, и параметры нужно предварительно получить специальным API-методом messages.getLongPollServer), и соединение зависает на время не более указанного в параметре wait кол-ва секунд (документация рекомендует устанавливать этот параметр на 25). Если до истечения этого интервала происходит какое-то событие (пришло новое сообщение, кто-то из друзей вышел в онлайн), немедленно приходит ответ; если же за 25 секунд ничего не происходит, то по истечении этого времени приходит пустой ответ. После получения ответа нужно отправить новый запрос, и так до бесконечности.

В описанном выше запросе на получение обновлений используется параметр key - ключ, время действия которого ограничено 3-4 часами; когда это время проходит, запрос начинает возвращать в ответ . В этом случае необходимо снова получить параметры запроса API-методом messages.getLongPollServer . Также присутствует параметр ts - что-то вроде идентификатора последней полученной порции обновлений; каждый long-polling запрос возвращает это значение, и его нужно использовать в следующем запросе, чтобы не получать обновления повторно.

Итак, основной класс. Сетевые запросы, пропатченные в em-synchrony , нельзя выполнять в корневом файбере, поэтому приходится создавать отдельный файбер и запускать код в нем - для этого добавлен хелпер #in_fiber .

Как видно, Messenger#start запускает основной цикл получения обновлений, Messenger#stop его останавливает (это нужно при закрытии вебсокет-соединения, иначе после обновления страницы будет уже два бесконечных цикла), Messenger#send_message отправляет сообщение указанному пользователю, Messenger#load_previous_messages загружает последнюю историю сообщений от и к указанному пользователю, а Messenger#mark_as_read помечает сообщения прочитанными.

На этом бэк-энд готов, переходим к фронт-энду.

Фронт-энд

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

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

Также в меню добавим метки: для друга, который сейчас онлайн, и для счетчика непрочитанных входящих сообщений от этого человека.

Верстка основной страницы выглядит следующим образом:

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

Список друзей и связанные с ним методы будем хранить в глобальной переменной usersList , а работу с обновлениями из основного цикла организуем через глобальный объект feed .

При получении списка друзей вызываем usersList.load , который в свою очередь удаляет все элементы .loading и рендерит интерфейс:

При загрузке списка пользователей создается массив объектов класса User . Объекты этого класса должны уметь запрашивать историю сообщений, следить за кол-вом непрочитанных входящих сообщений от соответствующего друга, а также помечать их прочитанными (все сразу, т.к. это будет происходить при заходе пользователя на вкладку с этим другом).

Все сообщения пользователя сохраняются в @messages , это происходит в методе addMessage - как и работа со счетчиком и меню. Классу Message остается лишь вызвать @user.addMessage и отрендерить сообщение:

И последнее, что остается - обработка обновлений, постоянно поступающих с сервера.

Мессенджер готов. И вот как он выглядит:

Запуск

К сожалению, ВКонтакте позволяет использовать API-методы для работы с сообщениями только десктопным и мобильным приложениям, поэтому придется получить токен доступа вручную и хранить его в конфигурации приложения. Благодаря foreman можно положить токен в файл .env в корне приложения, и он будет передаваться в main.rb как переменная окружения.

Итак, чтобы получить токен, сначала нужно зарегистрировать свое приложение на ВКонтакте. На этой странице нужно выбрать тип приложения “Standalone-приложение” и задать имя. Далее видим страницу настроек приложения, здесь нам нужно только поле “ID приложения”.

Теперь можно получать токен. Запускаем любую ruby-REPL (я использую pry , можно также взять irb ) и подключаем гем vkontakte_api :

Дальше просто копируем полученный URL и идем по нему в браузере. Там будет страница, предлагающая подтвердить права приложения на доступ к друзьям и личным сообщениям, а также доступ в любое время (это важно, поскольку позволяет получить “вечный” токен - его больше не придется обновлять). После нажатия на кнопку “Разрешить” идет редирект на страницу с текстом “Login success” - при этом в URL страницы будет параметр access_token , который нам и нужен.

Копируем токен и вставляем в файл .env в следующем формате:

Теперь при запуске приложения foreman положит токен в переменную окружения TOKEN , и в lib/main.rb он будет доступен как ENV['TOKEN'] - откуда мы его и берем при создании клиента API.

Остается лишь открыть в браузере файл public/index.html . Сразу после загрузки страницы фронт-энд открывает вебсокет-соединение, получает список друзей, рендерит интерфейс и начинает ожидать новых сообщений. Мессенджер работает :)

Как всегда, код на гитхабе.

В получившемся приложении остается еще довольно много возможностей для доработки - показывать аттачменты к сообщениям (картинки, аудио, видео), находить и рендерить URL-ы в виде ссылок; но проект показывает, что асинхронно работать с ВКонтакте API довольно удобно.

📎📎📎📎📎📎📎📎📎📎