
На удивление, в русскоязычном сегменте интернета (и на Хабре в том числе) до сих пор крайне мало информации о PubNub. Между тем, основанный в 2010-м году калифорнийский стартап успел за последние семь лет вырасти в то, что сама компания называет Global Data Stream Network (DSN), а по факту – IaaS-решение, направленное на удовлетворение нужд в области передачи сообщений в реальном времени. Наша компания – Distillery – является одним из на данный момент четырех development-партнеров PubNub, но сказано это не пустого бахвальства ради, а чтобы поделиться с сообществом вариантом использования PubNub на примере demo-проекта, который требовалось создать для получение оного статуса.
Те, кому не терпится посмотреть на код (C# + JavaScript), могут сразу пройти в репозиторий на GitHub. Тех же, кому интересно, что умеет PubNub, и как это работает, прошу под кат.
В целом PubNub предлагает три категории сервисов:
- Realtime Messaging. API, реализующий механизм Publish/Subscribe, за которым стоит готовая глобальная инфраструктура, включающая в себя 15 распределенных по земному шару локаций с заявленным latency не более 250мс. Все это приправлено такими вкусными вещами как, например, поддержка высоконагруженных каналов, компрессия данных и автоматический бандлинг сообщений при нестабильной связи.
- Presence. API для отслеживания состояния клиентов – от банального статуса онлайн/оффлайн до кастомных вещей вроде нотификаций о наборе сообщения.
- Functions. Раньше эта фу��кция называлась BLOCKS, но совсем недавно пережила ребрэндинг (точнее, все еще его переживает). Представляет собой скрипты, написанные на JavaScript и крутящиеся на серверах PubNub, с помощью которых можно фильтровать, агрегировать, трансформировать данные или, как мы вскоре увидим, осуществлять взаимодействие со сторонними сервисами.
Для реализации всего это дела PubNub предлагает более 70-ти SDK для самых разнообразных языков программирования и платформ, в том числе и для IoT-решений на базе Arduino, RaspberryPi и даже Samsung Smart TV (полный список можно найти тут).
Пожалуй, достаточно теории, перейдем к практике. Тестовое задание, предваряющее получение партнерского статуса, звучит следующим образом: «Создать проект на базе PubNub, используя два любых SDK и следующие функции: Presence, PAM и один BLOCK». PAM расшифровывается как PubNub Access Manager и является надстройкой над фреймворком безопасности, позволяющей контролировать доступ к каналу на уровне приложения, самого канала или конкретного пользователя. Поскольку задание сформулировано довольно расплывчато, это предоставляет достаточную волю фантазии, полет которой в итоге привел к не самой полезной, но весьма интересной идее говорящего чата. А чтобы было веселее, чат не просто озвучивается синтезатором речи, но еще и позволяет передавать вербальные эмоции.
Собственно, само приложение концептуально простое донельзя – это двухстраничный веб-сайт. Изначально пользователь попадает на страницу логина, где и настоящей аутентификации-то на самом деле не происходит, и после ввода никнейма и выбора режима – полный или ReadOnly – переходит на страницу с чатом. На ней имеется «окно» с сообщениями канала, в том числе и системными а ля «Vasya joined the channel», поле для набора сообщений и выпадающий список с выбором эмоций. При получении новых сообщений от других пользователей оные сообщения зачитываются синтезатором речи с той эмоцией, которая была выставлена автором при отправке. Для перевода текста в речь используется стандартный BLOCK от IBM Watson, требующий минимальной настройки, в основном касающейся используемого голоса. На момент написания статьи эмоциональную речь поддерживали только три голоса: en-US_AllisonVoice (женский), en-US_LisaVoice (женский) и en-US_MichaelVoice (мужской). Еще пару месяцев назад делать это умела только Allison, так что, как говорится, прогресс налицо.
Однако перейдем к коду. Серверная часть, и в этом прелесть, балансирует где-то на грани между простотой и примитивностью:
public class HomeController : Controller { public ActionResult Login() { return View(); } [HttpPost] public ActionResult Main(LoginDTO loginDTO) { String chatChannel = ConfigurationHelper.ChatChannel; String textToSpeechChannel = ConfigurationHelper.TextToSpeechChannel; String authKey = loginDTO.Username + DateTime.Now.Ticks.ToString(); var chatManager = new ChatManager(); if (loginDTO.ReadAccessOnly) { chatManager.GrantUserReadAccessToChannel(authKey, chatChannel); } else { chatManager.GrantUserReadWriteAccessToChannel(authKey, chatChannel); } chatManager.GrantUserReadWriteAccessToChannel(authKey, textToSpeechChannel); var authDTO = new AuthDTO() { PublishKey = ConfigurationHelper.PubNubPublishKey, SubscribeKey = ConfigurationHelper.PubNubSubscribeKey, AuthKey = authKey, Username = loginDTO.Username, ChatChannel = chatChannel, TextToSpeechChannel = textToSpeechChannel }; return View(authDTO); } }
Метод контроллера Main получает DTO от формы логина, извлекает информацию о каналах из конфигурационных данных (один канал для чата, второй для общения с IBM Watson), устанавливает уровень доступа посредством вызова соответствующих методов объекта класса ChatManager и отдает всю собранную информацию странице. Дальше всем занимается уже фронтенд. Для полноты картины приведем также листинг класса ChatManager, инкапсулирующего взаимодействие с SDK PubNub:
public class ChatManager { private const String PRESENCE_CHANNEL_SUFFIX = "-pnpres"; private Pubnub pubnub; public ChatManager() { var pnConfiguration = new PNConfiguration(); pnConfiguration.PublishKey = ConfigurationHelper.PubNubPublishKey; pnConfiguration.SubscribeKey = ConfigurationHelper.PubNubSubscribeKey; pnConfiguration.SecretKey = ConfigurationHelper.PubNubSecretKey; pnConfiguration.Secure = true; pubnub = new Pubnub(pnConfiguration); } public void ForbidPublicAccessToChannel(String channel) { pubnub.Grant() .Channels(new String[] { channel }) .Read(false) .Write(false) .Async(new AccessGrantResult()); } public void GrantUserReadAccessToChannel(String userAuthKey, String channel) { pubnub.Grant() .Channels(new String[] { channel, channel + PRESENCE_CHANNEL_SUFFIX }) .AuthKeys(new String[] { userAuthKey }) .Read(true) .Write(false) .Async(new AccessGrantResult()); } public void GrantUserReadWriteAccessToChannel(String userAuthKey, String channel) { pubnub.Grant() .Channels(new String[] { channel, channel + PRESENCE_CHANNEL_SUFFIX }) .AuthKeys(new String[] { userAuthKey }) .Read(true) .Write(true) .Async(new AccessGrantResult()); } }
Здесь имеет смысл заострить внимание на константе PRESENCE_CHANNEL_SUFFIX. Дело в том, что механизм Presence для своих сообщений использует отдельный канал, который по имеющемуся соглашению утилизирует имя текущего канала с добавлением суффикса «-pnpres». Обратите внимание, что код PubNub Access Manager, выраженный в виде вызова функции Grant, требует явного указания Presence-канала для установки прав доступа.
var pubnub; var chatChannel; var textToSpeechChannel; var username; function init(publishKey, subscribeKey, authKey, username, chatChannel, textToSpeechChannel) { pubnub = new PubNub({ publishKey: publishKey, subscribeKey: subscribeKey, authKey: authKey, uuid: username }); this.username = username; this.chatChannel = chatChannel; this.textToSpeechChannel = textToSpeechChannel; addListener(); subscribe(); }
Первое, что нам предстоит сделать в JavaScript-коде – это провести инициализацию соответствующего SDK. Для удобства и простоты некоторые сущности вынесены в глобальные переменные. После инициализации необходимо добавить слушателя для интересующих нас событий и подписаться на каналы чата, Presence и IBM Watson. Начнем с подписки:
function subscribe() { pubnub.subscribe({ channels: [chatChannel, textToSpeechChannel], withPresence: true }); }
Если код метода subscribe говорит сам за себя, то с методом addListener все немного сложнее:
function addListener() { pubnub.addListener({ status: function (statusEvent) { if (statusEvent.category === "PNConnectedCategory") { getOnlineUsers(); } }, message: function (message) { if (message.channel === chatChannel) { var jsonMessage = JSON.parse(message.message); var chat = document.getElementById("chat"); if (chat.value !== "") { chat.value = chat.value + "\n"; chat.scrollTop = chat.scrollHeight; } chat.value = chat.value + jsonMessage.Username + ": " + jsonMessage.Message; } else if (message.channel === textToSpeechChannel) { if (message.publisher !== username) { var audio = new Audio(message.message.speech); audio.play(); } } }, presence: function (presenceEvent) { if (presenceEvent.channel === chatChannel) { if (presenceEvent.action === 'join') { if (!UserIsOnTheList(presenceEvent.uuid)) { AddUserToList(presenceEvent.uuid); } PutStatusToChat(presenceEvent.uuid, "joins the channel"); } else if (presenceEvent.action === 'timeout') { if (UserIsOnTheList(presenceEvent.uuid)) { RemoveUserFromList(presenceEvent.uuid); } PutStatusToChat(presenceEvent.uuid, "was disconnected due to timeout"); } } } }); }
Во-первых, мы подписываемся на событие «PNConnectedCategory», чтобы отловить момент присоединения к каналу текущего пользователя. Это важно, потому что получение и отображение списка всех участников необходимо вызывать лишь однажды, в то время как Presence-событие «join» срабатывает каждый раз при присоединении нового клиента. Во-вторых, при поимке события о новом сообщении, мы проверяем канал, которому это событие адресовано, и в зависимости от результата проверки либо формируем текстовое представление путем банальной конкатенации, либо инициализируем объект Audio пришедшей от IBM Watson ссылкой на аудио-файл и запускаем проигрывание.
Еще одна интересная вещь происходит при отправке сообщения:
function publish(message) { var jsonMessage = { "Username": username, "Message": message }; var publishConfig = { channel: chatChannel, message: JSON.stringify(jsonMessage) }; pubnub.publish(publishConfig); var emotedText = '<speak>'; var selectedEmotion = iconSelect.getSelectedValue(); if (selectedEmotion !== "") { emotedText += '<express-as type="' + selectedEmotion + '">'; } emotedText += message; if (selectedEmotion !== "") { emotedText += '</express-as>'; } emotedText += '</speak>'; jsonMessage = { "text": emotedText }; publishConfig = { channel: textToSpeechChannel, message: jsonMessage }; pubnub.publish(publishConfig); }
Сначала мы формируем само сообщение, затем определяем конфигурацию, которую понимает SDK, и только после этого инициируем отправку. Дальше лучше. Чтобы превратить текст в синтезированную речь, еще одно сообщение мы отправляем в канал IBM Watson. Для определения эмоциональной окраски используется Speech Synthesis Markup Language (SSML), а если конкретнее — тэг <express-as>. Как вы уже наверняка догадываетесь, при отправке сообщения ReadOnly-пользователем, оно будет заблокировано механизмом PAM и так никогда и не найдет своего получателя.
Среди уже имеющихся на рынке продуктов, использующих возможности PubNub, можно отметить, скажем, концепцию умного дома от Insteon или мобильное приложение для планирования семейных мероприятий от Curago. В завершении еще раз напомню, что полный код примера можно найти на GitHub.