В начале этого года мне понадобилось работать с API ВКонтакте из rails-приложения. Увы, я не нашел сколько-нибудь устраивающего меня гема: где-то меня принуждали писать названия методов в camelCase (что в ruby-коде выглядит неестественно), где-то — обязательно проходить авторизацию через библиотеку (при том, что я использовал omniauth) и вообще везде для обращений к API использовался захардкоденный Net::HTTP
, блокирующий реактор эвентмашины, на которую я тогда прицеливался. Также в плане документации почему-то все было очень грустно, и приходилось постоянно читать исходники.
Так появился на свет vkontakte_api. Рельсовый проект, послуживший поводом для написания данной библиотеки, уже успел почить — но гем живет и продолжает развиваться, в июле достигнув версии 1.0 (которая послужила поводом для значительных изменений). Используя faraday
, библиотека поддерживает вызов любых методов API, загрузку файлов на сервера ВКонтакте и опциональную авторизацию, не принимая за программиста решения, упомянутые в предыдущем абзаце.
Посмотрим, как работать с API с помощью vkontakte_api
. В качестве примера сгодится несложное веб-приложение, отображающее на странице ленту новостей (API-метод newsfeed.get), список друзей (friends.get) и групп (groups.get) пользователя, прошедшего OAuth2-авторизацию. А выглядеть это будет примерно так:
Настройка
В авторизации используются ID приложения и защищенный ключ, которые можно получить на странице редактирования приложения на ВКонтакте; а также redirect_uri, который будет описан далее. Эти параметры указываются в блоке VkontakteApi.configure
, который удобно разместить в config/initializers/vkontakte_api.rb
; в rails-приложении можно сгенерировать этот файл с настройками по умолчанию с помощью встроенного генератора.
$ rails generate vkontakte_api:install
Настройки указываются следующим образом.
# config/initializers/vkontakte_api.rb
VkontakteApi.configure do |config|
config.app_id = '123' # ID приложения
config.app_secret = 'AbCdE654' # защищенный ключ
config.redirect_uri = 'http://vkontakte-on-rails.herokuapp.com/callback'
end
(на самом деле доступных настроек гораздо больше, но остальные тут не понадобятся)
Авторизация
Входить на сайт понадобится только через ВКонтакте, поэтому задействовать omniauth
будет нецелесообразно — используем возможности vkontakte_api
.
Авторизация приложения на ВКонтакте использует протокол OAuth2. Это означает, что в результате авторизации будет получен токен доступа, который необходимо передавать при вызове методов API.
Схема его получения следующая: пользователь переходит по ссылке на страницу авторизации на ВКонтакте, соглашается дать приложению доступ к его (пользователя) данным, нажимая кнопку «Разрешить», и ВКонтакте редиректит его обратно в приложение, передавая в URL-е параметр code
. Далее приложение, используя этот код, получает токен и user_id
пользователя отдельным запросом и сохраняет их в сессии.
Для защиты от CSRF-атак протокол OAuth2 рекомендует передавать параметр state
с неугадываемым значением при отправке пользователя на авторизацию, предварительно сохранив его в защищенном месте; а при возвращении пользователя сверять полученный в параметрах state
с сохраненным значением.
Итак, на странице входа нужно отобразить ссылку, ведущую на страницу авторизации приложения на ВКонтакте. vkontakte_api
предоставляет хелпер VkontakteApi.authorization_url
для генерации URL этой страницы; в параметрах нужно передать scope
— это права, которые получит приложение, в виде массива символов (или же строки с названиями, разделенными запятыми) — и описанный выше state
.
# app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
def new
# генерируем случайный state
srand
session[:state] ||= Digest::MD5.hexdigest(rand.to_s)
# и URL страницы авторизации
@vk_url = VkontakteApi.authorization_url(scope: [:friends, :groups, :offline, :notify], state: session[:state])
end
end
<!-- app/views/sessions/new.html.erb -->
<%= link_to @vk_url, class: 'btn btn-primary' do %>
<i class="icon-home icon-white"></i>
Войти через ВКонтакте
<% end %>
Тут нужно заметить, что по непонятным причинам ВКонтакте игнорирует state
, если в scope
не указан notify
.
Когда пользователь подтвердит права приложения, он будет перенаправлен на указанный ранее в настройках redirect_uri
(содержащий путь к SessionsController#callback
), при этом в URL будут переданы параметры state
и code
. Как говорилось чуть выше, state
нужно сверить с уже сохраненным; а на code
остановимся поподробнее.
С помощью кода можно получить токен доступа, для этого нужно выполнить запрос к ВКонтакте. Пользователь в этом запросе никак не участвует — запрос идет прямо от нашего сервера к vk.com. Для этого vkontakte_api
также предоставляет хелпер — VkontakteApi.authorize
, единственный параметр — пресловутый code
.
# encoding: utf-8
class SessionsController < ApplicationController
def callback
# проверка state
if session[:state].present? && session[:state] != params[:state]
redirect_to root_url, alert: 'Ошибка авторизации, попробуйте войти еще раз.' and return
end
# получение токена
@vk = VkontakteApi.authorize(code: params[:code])
# и сохранение его в сессии
session[:token] = @vk.token
# также сохраним id пользователя на ВКонтакте - он тоже пригодится
session[:vk_id] = @vk.user_id
redirect_to root_url
end
end
При выходе пользователя из нашего приложения просто почистим сессию:
class SessionsController < ApplicationController
def destroy
session[:token] = nil
session[:vk_id] = nil
redirect_to root_url
end
end
Токен получен, можно работать с самим API.
Вызов методов API
Чтобы вызывать методы API, нужен объект VkontakteApi::Client
. В конструктор нужно просто передать токен.
Далее можно вызывать методы на самом клиенте. Методы с составными именами вызываются по цепочке: vk.users.get(params)
. В соответствии с принятыми в ruby-сообществе соглашениями названия методов пишутся в snake_case
: метод API likes.getList
можно вызвать как vk.likes.get_list
.
Все параметры API являются именованными и передаются в виде хэша, проиндексированного названиями параметров, например vk.users.get(uid: 1)
. Если API ожидает получить в параметре коллекцию объектов, перечисленных через запятую, то их можно передать в виде массива — vkontakte_api
склеит его автоматически (аналогично обрабатывается параметр scope
в авторизации). При этом вместо строк можно использовать символы.
Итак, нам нужна лента новостей, друзья и группы текущего пользователя. Также выведем имя и аватар пользователя в навигации. Для получения этих данных есть методы newsfeed.get
, friends.get
, groups.get
и users.get
соответственно (последний будем вызывать, передавая параметром id нашего пользователя). Результат newsfeed.get
содержит отдельно сами новости, содержащие id пользователей и групп, и отдельно массивы с упомянутыми пользователями и группами; не показанный здесь метод MainController#process_feed
добавляет к каждой новости ее источник (пользователь или группа, написавшая пост) под ключом source
.
class MainController < ApplicationController
def index
# сначала создадим клиент API
vk = VkontakteApi::Client.new(session[:token])
# теперь получим текущего юзера
@user = vk.users.get(uid: session[:vk_id], fields: [:screen_name, :photo]).first
# его друзей
@friends = vk.friends.get(fields: [:screen_name, :sex, :photo, :last_seen])
# отдельно выберем тех, кто в данный момент онлайн
@friends_online = @friends.select { |friend| friend.online == 1 }
# группы
@groups = vk.groups.get(extended: 1)
# первый элемент массива - кол-во групп; его нужно выкинуть
@groups.shift
# и ленту новостей
raw_feed = vk.newsfeed.get(filters: 'post')
# обработанную в отдельном методе
@newsfeed = process_feed(raw_feed)
end
end
Результаты методов возвращаются в виде Hashie::Mash
— это расширение стандартного Hash
из гема hashie, позволяющее обращаться к элементу через метод, название которого соответствует ключу этого элемента в хэше (user.name == user[:name]
).
В навигации нужно показать аватар и имя текущего пользователя, полученные с ВКонтакте.
<%= link_to vk_url(@user), target: '_blank' do %>
<%= image_tag(@user.photo, width: 20) %>
<%= "#{@user.first_name} #{@user.last_name}" %>
<% end %>
Здесь и далее используется ряд несложных хелперов (vk_url
, name_for
, avatar_for
итд), определенных в приложении — все они достаточно тривиальны, при желании можно почитать код здесь.
Теперь выведем на страницу ленту новостей.
<!-- app/views/main/index.html.erb -->
<% @newsfeed.each do |item| %>
<tr>
<td>
<%= link_to vk_url(item.source), target: '_blank' do %>
<%= image_tag avatar_for(item.source) %>
<% end %>
</td>
<td class="wide">
<div class="pull-right"><%= formatted_time_for(item.date) %></div>
<%= link_to name_for(item.source), vk_url(item.source), target: '_blank' %>
<p><%=raw render_links(item.text) %></p>
<% item.attachments.each do |attachment| %>
<%= render 'attachment', attachment: attachment %>
<% end if item.attachments? %>
</td>
</tr>
<% end %>
<!-- app/views/main/_attachment.html.erb -->
<p>
<% case attachment.type %>
<% when 'link' %>
<%= link_to attachment.link.title, attachment.link.url, target: '_blank' %>
<% when 'photo' %>
<%= image_tag attachment.photo.src_big %>
<% when 'video' %>
<%= image_tag attachment.video.image_big %>
<% end %>
</p>
<div class="clearfix"></div>
И, наконец, отобразим в сайд-баре друзей и группы пользователя.
<!-- app/views/main/_sidebar.html.erb -->
<div class="tab-pane active" id="friends_online">
<h6>Друзья онлайн</h6>
<%= render 'friends', friends: @friends_online %>
</div>
<div class="tab-pane" id="friends">
<h6>Все друзья</h6>
<%= render 'friends', friends: @friends %>
</div>
<div class="tab-pane" id="groups">
<h6>Группы</h6>
<%= render 'groups' %>
</div>
<!-- app/views/main/_friends.html.erb -->
<table class="table">
<% if friends.empty? %>
<tr>
<td>Никого не найдено</td>
</tr>
<% else %>
<% friends.each do |friend| %>
<tr>
<td>
<%= link_to image_tag(friend.photo), vk_url(friend), target: '_blank' %>
</td>
<td class="wide">
<i class="icon-user"></i>
<%= link_to "#{friend.first_name} #{friend.last_name}", vk_url(friend), target: '_blank' %>
<br />
<%= online_status(friend) %>
</td>
</tr>
<% end %>
<% end %>
</table>
<!-- app/views/main/_groups.html.erb -->
<table class="table">
<% if @groups.empty? %>
<tr>
<td>Вы не состоите в группах</td>
</tr>
<% else %>
<% @groups.each do |group| %>
<tr>
<td>
<%= link_to image_tag(group.photo), vk_url(group), target: '_blank' %>
</td>
<td class="wide">
<i class="icon-comment"></i>
<%= link_to group.name, vk_url(group), target: '_blank' %>
</td>
</tr>
<% end %>
<% end %>
</table>
Живое демо можно посмотреть здесь (осторожно, бесплатный heroku). Оно ничего не пишет на ВКонтакте — все методы API используются только для чтения данных и вывода их на страницу. Весь код лежит на Github.
Еще немного материалов по vkontakte_api
- домашняя страница проекта
- README.md
- RDoc-документация со 100% покрытием
- более продвинутый пример использования vkontakte_api (асинхронное приложение-мессенджер на вебсокетах)
Автор: 7even