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

  1. завдання
  2. Реалізація
  3. Бек-енд
  4. Фронт-енд
  5. запуск
  6. PS
  7. PPS

У широко відомої соцмережі ВКонтакте є трохи менш відомий 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 після цього):

Gemfile (Gemfile) download 1 2 3 4 5 6 7 8 9 source: rubygems gem 'eventmachine' gem 'em-synchrony' gem 'em-websocket' gem 'em-http-request' gem 'vkontakte_api', '~> 1.0' gem 'foreman'

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

Procfile (Procfile) download

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

lib / main.rb (main.rb) download 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 require 'bundler' Bundler. require require_relative 'messenger' # потрібно вимкнути буферизацию виведення, # щоб бачити логгірованіе в реальному часі $ stdout. sync = true VkontakteApi. configure do | config | # Здійснюємо запити через em_synchrony-адаптер config. adapter =: em_synchrony # в основному циклі отримання повідомлень з'єднання будуть # висіти до 25 секунд, тому ставимо таймаут на півхвилини config. faraday_options = {request: {timeout: 30}} end # створюємо клієнт API, через нього будемо відправляти всі запити до ВКонтакте $ client = VkontakteApi :: Client. new (ENV [ 'TOKEN']) EM. synchrony do EventMachine :: WebSocket. start (host: '0.0.0.0', port: 8080) do | ws | ws. onopen do # при відкритті з'єднання з браузером створюємо новий месенджер VkontakteApi. logger. debug 'Connection open' $ messenger = Messenger. new (ws) $ messenger. start end ws. onclose do # при закритті з'єднання зупиняємо месенджер, # щоб він перестав запитувати поновлення $ messenger. stop VkontakteApi. logger. debug 'Connection closed' end ws. onmessage do | msg | # Повідомлення приходить в форматі uid = 12345 & message = abcde # Парс його в Hashie :: Mash і відправляємо месенджер data = CGI. parse (msg). inject (Hashie :: Mash. new) do | mash, (key, value) | mash. merge (key => value. first) end VkontakteApi. logger. debug "Received message: # {data. inspect}" action = data. delete (: action) if% w [send_message load_previous_messages mark_as_read]. include? (Action) $ messenger. send (action, data) end end end end

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

В описаному вище запиті на отримання оновлень використовується параметр key - ключ, час дії якого обмежено 3-4 годинами; коли цей час проходить, запит починає повертати у відповідь { ​​"failed": 2}. В цьому випадку необхідно знову отримати параметри запиту API-методом messages.getLongPollServer. Також присутній параметр ts - щось на зразок ідентифікатора останньої отриманої порції оновлень; кожен long-polling запит повертає це значення, і його потрібно використовувати в наступному запиті, щоб не отримувати оновлення повторно.

Отже, основний клас. Мережеві запити, пропатченні в em-synchrony, не можна виконувати в кореневому файбер, тому доводиться створювати окремий файбер і запускати код в ньому - для цього доданий хелпер #in_fiber.

lib / messenger.rb (messenger.rb) download 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 class Messenger # зберігаємо об'єкт EventMachine :: WebSocket :: Connection # в інстанс- змінної, щоб відправляти дані на фронт-енд def initialize (ws) @ws = ws end # запуск нескінченного циклу отримання оновлень def start in_fiber do # отримуємо список друзів friends = $ client. friends. get (fields: [: screen_name,: photo]) # і кількість непрочитаних повідомлень, розбите по відправникам unread = get_unread_messages # складаємо в один хеш friends. each do | friend | friend. unread = unread [friend. uid] end # і відправляємо фронт-енду, щоб той отрендеріть інтерфейс send_to_websocket (friends_list: friends) # отримуємо параметри long-polling url, params = get_polling_params # і робимо запити, поки месенджер не зупинено while self. running? && response = VkontakteApi :: API. connection. get (url, params). body if response. failed? # Час дії ключа минув, потрібно отримати новий url, params = get_polling_params next else # все нормально - відправляємо поновлення на фронт-енд send_to_websocket (updates: response. Updates) # і оновлюємо параметр ts для використання # в наступному запиті до ВКонтакте params. ts = response. ts end end end end # зупинка месенджера def stop @stopped = true end # запущений месенджер def running? ! @stopped end # відправка повідомлення користувачу def send_message (params = {}) in_fiber do # міні-милицю для виклику $ client.messages.send # тому метод: send визначено в Kernel VkontakteApi :: Method. new ( 'send', resolver: $ client. messages). call (params) end end # завантаження повідомлень, відправлених # до запуску мессенджера def load_previous_messages (params = {}) in_fiber do # викидаємо перший елемент масиву (там буде загальна кількість повідомлень) # і сортуємо в хронологічному порядку messages = $ client. messages. get_history (uid: params. uid). tap (&: shift). reverse data = {uid: params. uid, messages: messages} # відправляємо повідомлення на фронт-енд з типом previous_messages send_to_websocket (previous_messages: data) end end # позначка повідомлень прочитаними def mark_as_read (params = {}) in_fiber do $ client. messages. mark_as_read (mids: params. mids) # на фронт-енд тут нічого відправляти не потрібно, # тому зміна статусу "прочитано" прийде в основному циклі end end private # хелпер для відправки даних в веб-сокет def send_to_websocket (messages) messages. each do | type, data | # Якщо data - хеш, то перетворюємо його символьні ключі в рядкові # (щоб не отримати після JSON-кодування ": messages") data = data. inject ({}) do | hash, (key, value) | hash [key. to_s] = value hash end if data. is_a? (Hash) json = Oj. dump ( 'type' => type. to_s, 'data' => data) @ws. send json end end # отримання параметрів для long-polling запиту def get_polling_params params = $ client. messages. get_long_poll_server [ 'http: //' + params. delete (: server), params. merge (act: 'a_check', wait: 25, mode: 2)] end # кількість непрочитаних вхідних повідомлень, розбите по відправникам def get_unread_messages messages = $ client. messages. get (filters: 1) # знову викидаємо перший елемент за непотрібністю messages. shift # і складаємо все в хеш, проіндексований по id відправника counts = Hash. new (0) messages. inject (counts) do | hash, message | hash [message. uid] + = 1 hash end end # хелпер для запуску коду в окремому файбер def in_fiber (& block) Fiber. new (& block). resume end end

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

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

Фронт-енд

По-перше, не довго думаючи, візьмемо Twitter Bootstrap для створення інтерфейсу, щоб не витрачати на це зайвий час і нерви. По-друге, з тих же причин, використовуємо CoffeeScript для програмування клієнтської частини.

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

Також в меню додамо мітки: для одного, який зараз онлайн, і для лічильника непрочитаних вхідних повідомлень від цієї людини.

Верстка основної сторінки виглядає наступним чином:

public / index.html (index.html) download 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 <! DOCTYPE html> <html> <head> <title> VkontakteOnEM </ title> <link rel = "stylesheet" href = "css / bootstrap.min.css" > <link rel = "stylesheet" href = "css / main.css"> <script src = "http://ajax.googleapis.com/ajax/libs/jquery/1.7.0/jquery.min.js"> </ script> <script src = "js / bootstrap.min.js"> </ script> <script src = "js / main.js" charset = "utf-8"> </ script> </ head> < body> <div class = "container"> <div class = "row"> <div class = "span12"> <h2> VKontakte messenger </ h2> </ div> </ div> <div class = "row" > <div class = "span3"> <div class = "well"> <ul class = "nav nav-list" id = "navbar"> <li class = "active"> <a href = "#debug" data -toggle = "tab"> <i class = "icon-info-sign"> </ i> Debug info </a> </ li> <li> <a href = "#feed" data-toggle = "tab "> <i class =" icon-th-list "> </ i> Feed </a> </ li> <li class =" divider "> </ li> <Li class = "loading"> Loading friends list ... </ li> </ ul> </ div> </ div> <div class = "span9"> <div class = "tab-content" id = " main "style =" width: 100%; " > <Div class = "tab-pane fade in active" id = "debug"> <h6> Debug info </ h6> <div class = "hero-unit loading"> <h2> Loading ... </ h2> </ div> </ div> <div class = "tab-pane fade" id = "feed"> <h6> Activity feed </ h6> <ul class = "feed"> </ ul> </ div> < / div> </ div> </ div> </ div> </ body> </ html>

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

public / js / main.coffee (main.coffee) download 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 # хелпер для логгірованія log = (param) -> console. log param $ (document). ready -> # обробник переходу в табу користувача $ ( '#navbar'). on 'shown', 'li.user a [data-toggle = "tab"]', (e) -> user_id = e. target. id. split ( '_') [1] user = usersList. list [user_id] # якщо попередні повідомлення ще не завантажено, вантажимо user. loadPreviousMessages () unless user. previousMessagesLoaded # якщо є непрочитані повідомлення - помічаємо прочитаними user. markAllAsRead () # обробник сабмита форми $ (document). on 'submit', 'form.message', (e) -> form = $ (e. target) message = action: 'send_message' uid: form. data ( 'user-id ") message: form [0]. message. value ws. send $. param (message) form [0]. message.value = '' false window. ws = new WebSocket ( 'ws: //0.0.0.0: 8080') # обробник повідомлень від бек-енду ws.onmessage = (event) -> message = $. parseJSON event. data switch message. type when 'friends_list' usersList. load message. data when 'previous_messages' usersList. list [message. data. uid]. loadPreviousMessages message. data. messages when 'updates' feed. process message. data else log 'received unknown message:' log message $ ( '#debug'). append '<pre>' + event. data + '</ pre>' ws.onopen = -> log 'connected ...' ws.onclose = -> log 'socket closed'

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

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

public / js / users_list.coffee (users_list.coffee) download 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 window. usersList = list: {} load: (list) -> for user_attributes in list @list [user_attributes. uid] = new User (user_attributes) @clearOnLoad () @renderMenu () @renderPanes () # очищення і ререндер менюшки renderMenu: -> # запам'ятовуємо активну табу activeTabId = $ ( 'li.active a'). attr ( 'id') # чистимо меню $ ( '#navbar .user'). remove () # наповнюємо заново for id, user of @list li = '<li class = "user"> <a href = "# user_' + id + '" id = "tab_' + id + '" data-toggle = "tab"> 'li + =' <i class = "icon-user"> </ i> '+ user. name li + = '<span class = "label label-success"> Online </ span>' if user. online li + = '<span class = "badge badge-warning">' + user. unreadCount () + '</ span>' if user. hasUnread () li + = '</a> </ li>' $ ( '#navbar'). append li # відновлюємо активну табу $ ( '#' + activeTabId). parent (). addClass ( 'active') # метод повинен викликатися один раз після завантаження usersList renderPanes: -> for id, user of @list pane = '<div class = "tab-pane fade user" id = "user_' + id + '" > 'pane + =' <h6> '+ user. name + '</ h6> <ul class = "feed"> </ ul>' pane + = '<form class = "well message" data-user-id = "' + id + '">' pane + = '<textarea class = "span8" name = "message" placeholder = "Повідомлення"> </ textarea>' pane + = '<button class = "btn btn-primary" type = "submit"> Надіслати </ button>' pane + = '</ form> </ div>' $ ( '#main'). append pane clearOnLoad: -> $ ( '.loading'). remove ()

При завантаженні списку користувачів створюється масив об'єктів класу User. Об'єкти цього класу повинні вміти запитувати історію повідомлень, стежити за кол-вом непрочитаних вхідних повідомлень від відповідного одного, а також позначати їх прочитаними (все відразу, тому що це буде відбуватися при заході користувача на вкладку з цим другом).

public / js / user.coffee (user.coffee) download 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 class User constructor: (data) - > @uid = data. uid @name = [data. first_name, data. last_name]. join ( '') @online = data. online # лічильник непрочитаних повідомлень, # потрібний на той час, поки @previousMessagesLoaded = false; # Потім підрахунок йде по @messages @unread = data. unread @messages = {} @previousMessagesLoaded = false # перевантажена функція: # без параметрів запитує повідомлення у бек-енду; # Коли той відповідає, ця ж функція викликається # з отриманими повідомленнями, і відбувається рендеринг loadPreviousMessages: (messages) -> if messages? # Очищаємо все вже завантажені @messages = {} $ ( "#user _ # {@ uid} ul.feed"). html ( '') # і Рендер отримані повідомлення for message in messages unread = if (message. read_state == 1) then 0 else 1 flags = unread + message. out * 2 params = [message. mid flags @uid message. date null message. body message. attachments] # ​​повідомлення рендерится в конструкторі Message new Message (params ...) @previousMessagesLoaded = true # якщо відкрита таба цього користувача, # потрібно відразу помітити всі повідомлення прочитаними @markAllAsRead () if @paneActive () else # запитуємо повідомлення з вебсокета data = action: 'load_previous_messages' uid: @uid ws. send $. param (data) unreadCount: -> # кількість непрочитаних вхідних повідомлень вважається по-різному # в залежності від @previousMessagesLoaded: if @previousMessagesLoaded # коли попередні повідомлення вже завантажені в @messages, # обходимо їх і підраховуємо @unreadMessagesIds (). length else # до завантаження попередніх повідомлень вважаємо за @unread, отриманої # при початкової завантаженні списку користувачів @unread hasUnread: -> @unreadCount ()> 0 paneActive: -> $ ( "#user _ # {@ uid}"). hasClass ( 'active') unreadMessagesIds: -> # повертаємо id непрочитаних вхідних повідомлень з @messages id for id, message of @messages when message. unreadAndIncoming () addMessage: (message) -> @messages [message. id] = message # якщо кількість непрочитаних вхідних змінилося, Рендер меню if message. unreadAndIncoming () # якщо попередні повідомлення ще не завантажено, збільшуємо лічильник @unread + = 1 if! @previousMessagesLoaded usersList. renderMenu () # якщо панель активна, відразу помічаємо всі повідомлення прочитаними @markAllAsRead () if @paneActive () markAllAsRead: -> if @previousMessagesLoaded and @hasUnread () message = action: 'mark_as_read' mids: @unreadMessagesIds (). join ( ',') ws. send $. param (message)

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

public / js / message.coffee (message.coffee) download 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 class Message constructor: (@id, flags, from_id, timestamp, @subject, @text, @attachments) -> @unread = !! (Flags & 1) @outgoing = !! (Flags & 2) @user = usersList. list [from_id] @date = new Date (timestamp * 1000) @user. addMessage this @render () unreadAndIncoming: -> @unread and! @outgoing # помічаємо ПОВІДОМЛЕННЯ прочитання в інтерфейсі додатка # (вікорістовується, коли ВКонтакте сообщает, что це ПОВІДОМЛЕННЯ прочитано) read: -> @unread = false # если ПОВІДОМЛЕННЯ входити, нужно заново отрендеріть меню unless @outgoing # а если у користувача Ще не завантажені попередні ПОВІДОМЛЕННЯ, # треба ще и відняті Одиниця з лічильника @user. unread - = 1 unless @user. previousMessagesLoaded usersList. renderMenu () render: -> classes = [ 'message'] classes. push 'pull-right' if @outgoing sender = if @outgoing then 'Я' else @user. name messageString = '<blockquote id = "' + @id + '" class = "' + classes. join ( '') + '">' messageString + = "<p> # {@ text} </ p>" messageString + = '<small> <i class = "icon-user"> </ i>' + sender messageString + = '| '+ Feed. formatDate (@date) + '</ small>' messageString + = '</ blockquote>' messageString + = '<div class = "clearfix"> </ div>' $ ( "#user_#{@user.uid} ul.feed "). append messageString

І останнє, що залишається - обробка оновлень, постійно надходять з сервера.

public / js / feed.coffee (feed.coffee) download 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 window. feed = process: (updates) -> for update in updates code = update. shift () switch code # зміна прапорів повідомлення when 3 [message_id, flags, user_id] = update usersList. list [user_id]. messages [message_id]. read () if flags & 1 # додавання нового повідомлення when 4 message = new Message (update ...) # друг став онлайн (8) / оффлайн (9) when 8, 9 user_id = update [0] user = usersList. list [- user_id] user.online = if code == 8 then 1 else 0 usersList. renderMenu () date = '<span class = "badge">' + @formatDate () + '</ span>' if code == 8 label = '<span class = "label label-info"> online </ span > 'else label =' <span class = "label label-important"> offline </ span> '@addStatus [date, label]. join ( ''), user # форматування дати в стилі "04.09.2012 в 23:50:16" або "сьогодні в 1:46:23" formatDate: (date = new Date) -> dateParts = [date. getDate () date. getMonth () + 1 date. getFullYear ()] today = new Date if dateParts [0] == today. getDate () and dateParts [1] == today. getMonth () + 1 and dateParts [2] == today. getFullYear () dateString = 'сьогодні' else dateParts = for part in dateParts if part. toString (). length is 1 then "0 # {part}" else part dateString = dateParts. join '.' timeParts = [date. getHours () date. getMinutes () date. getSeconds ()] timeParts = for part in timeParts if part. toString (). length is 1 then "0 # {part}" else part timeString = timeParts. join ':' "# {dateString} в # {timeString}" addStatus: (statusString, user) -> # додаємо повний запис (лейбли плюс юзернейм) до загального фид $ ( '#feed ul.feed'). append "<li> # {statusString} # {user.name} </ li>" # і вкорочений запис (тільки лейбли) в персональний фид $ ( "#user _ # {user.uid} ul.feed"). append "<li> # {statusString} </ li>"

Мессенджер готовий. І ось як він виглядає:

запуск

На жаль, ВКонтакте дозволяє використовувати API-методи для роботи з повідомленнями тільки десктопних і мобільних додатків, тому доведеться отримати токен доступу вручну і зберігати його в конфігурації програми. Завдяки foreman можна покласти токен в файл .env в корені додатки, і він буде передаватися в main.rb як змінна оточення.

Отже, щоб отримати токен, спочатку потрібно зареєструвати свій додаток на ВКонтакте. на цієї сторінці потрібно вибрати тип програми "Standalone-додаток" і задати ім'я. Далі бачимо сторінку налаштувань програми, тут нам потрібно тільки поле "ID додатки".

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

1 2 3 4 5 6 7 8 $ pry -r vkontakte_api # в app_id вписуємо ID додатки [1] pry (main)> VkontakteApi.app_id = '3074156' # тут потрібно обов'язково вказати http://oauth.vk.com/blank .html [2] pry (main)> VkontakteApi.redirect_uri = 'http://oauth.vk.com/blank.html' # отримуємо URL авторизації [3] pry (main)> VkontakteApi.authorization_url (type:: client, scope: [: friends,: messages,: offline]) "https://oauth.vk.com/authorize?response_type=token&client_id=3074156&scope=friends%2Cmessages%2Coffline&redirect_uri=http%3A%2F%2Foauth.vk.com% 2Fblank.html "

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

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

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

1 2 3 4 # компілюємо кофескріпт в яваскрипт $ coffee -cj public / js / main.js public / js / {user, message, users_list, feed, main} .coffee # запускаємо бек-енд $ foreman start

Залишається лише відкрити в браузері файл public / index.html. Відразу після завантаження сторінки фронт-енд відкриває вебсокет-з'єднання, отримує список друзів, рендерить інтерфейс і починає чекати нових повідомлень. Месенджер працює :)

PS

Як завжди, код на гітхабе .

PPS

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

Include?
Running?
Failed?
Is_a?
Com/authorize?

Дополнительная информация

rss
Карта