vkontakte_api: ruby-адаптер для ВКонтакте API

в 14:15, , рубрики: ruby, Вконтакте API, метки: ,

В начале этого года мне понадобилось работать с 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-авторизацию. А выглядеть это будет примерно так:

vkontakte api: ruby адаптер для ВКонтакте API

Настройка

В авторизации используются 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

Автор: 7even


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js