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



    Настройка


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


    Поделиться публикацией
    Похожие публикации
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама
    Комментарии 27
    • 0
      Разлогинил ваше приложение через ВК и теперь вижу ошибку:

      We're sorry, but something went wrong.

      Или это бесплатные лимиты Heroku закончились?
      • 0
        У меня успешно залогинилось и разлогинилось, без ошибок.
        • 0
          А как вы разлогинивались?
          • 0
            Кнопкой в правом верхнем углу на сайте, а там были для этого другие контролы?
            • 0
              После удаления приложения на странице http://vk.com/apps?act=settings токен протухает, но продолжает висеть в куке к vkontakte-on-rails.herokuapp.com. По хорошему надо было бы проверять работоспособность токена в приложении, но я этого не сделал, чтобы не усложнять пример.

              Чтобы все снова заработало, нужно просто удалить эту куку.
              • 0
                А, в этом плане.

                Так не пробовал, да. Но в примере и правда не критично.
              • 0
                ВК → Приложения → Настройки → vkontakte_on_rails → убрать
        • 0
          А гем подружится с Devise если через провайдер vk вход выполнен?
          • 0
            Если имеется в виду интеграция Devise с Omniauth, то да — нужно просто вытащить токен из request.env['omniauth.auth'] (если верить доке, токен должен лежать в request.env['omniauth.auth']['credentials']['token']). После этого можно создать клиент API как-то так:

            vk = VkontakteApi::Client.new(token)
            
            • 0
              Спасибо за развернутый ответ.
          • 0
            черт, дизайн у вашего варианта мне нравится куда больше, чем у оригинала и бывшего durov.ru
            допилить бы в таком же духе сообщения (по сути больше ничего мне от контакта не нужно) и можно пользоваться. Жаль знание Руби у меня весьма поверхностное
          • 0
            Наконец-то, нормальный джем :) Я, правда, устал искать и свой велосипед написал :)
            • 0
              Скажите, это только у меня в процессе установки под виндой все падает на установке json-парсера oj (не компилируется и все тут)? Есть ли возможность сделать парсер подменяемым, чтобы использовать что-нибудь более стандартное?
              • 0
                Напишите лог ошибки. И будет вам решение (:
                • 0
                  Выложил лог. gist.github.com/3769663
                  Он пишет о двух ошибках — отсутствие pthread.h (с этим я справился, скачав библиотеку и запихнув её в папку установки гема)
                  И что-то о gettimeofday — кажется конфликт версий библиотеки у руби и девкита. Как это вылечить я сходу не понимаю
                • 0
                  Насколько я знаю, под виндой у всех проблемы с установкой Oj. Подменить парсер другим пока нельзя, ибо другие парсеры (как минимум все из комплекта multi_json) с вконтактовским JSON не справляются, см. 7even/vkontakte_api#1.

                  В планах на будущее — все-таки разобраться с проблемами парсинга, и я рассчитываю, что в итоге получится оставить выбор парсера программисту.
                  • 0
                    Что ж у них за JSON такой невалидный?) В любом случае спасибо. Будем мониторить проект.
                    • +3
                      Кстати, ура-ура, oj теперь компилируется и под виндой тоже.
                • –3
                  Если будет интересно, то могу выложить и написать пост про свой VK API класс для PHP.
                  • 0
                    Напишите — новое, это всегда интересно. А минусят, думаю, потому, что нужно просто выложить, а не спрашивать в чужой теме. :)
                  • 0
                    А что Вы думаете по поводу github.com/zinenko/vk-ruby в сравнении со своим решением? Я пока взял именно его, думают, стоит ли менять.
                    • 0
                      Когда я начинал писать vkontakte_api, vk-ruby не устраивал меня по разным причинам. Насколько я знаю, за последнее время этот проект тоже подтянулся, но все еще не поддерживает snake_case-названия методов и авторизацию standalone-приложений, плюс всякие мелочи вроде автоматического склеивания параметров-массивов через join(',').

                      Решать вам.
                      • 0
                        Авторизацию standalone вообще кто-нить нормально поддерживает?
                        Кроме гема, который парсит ВК
                        • +1
                          На уровне получения ссылки на страницу авторизации vkontakte_api поддерживает. Но надо понимать, что без контроля над адресной строкой браузера это сделать технически невозможно (не прибегая к парсингу). В десктопных и мобильных приложениях этот контроль есть, в веб-приложениях — нет.
                          • 0
                            Да, спасибо.
                            Я вообще руками получаю, задача редкая

                    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.