Используем PubNub: эмоциональный говорящий чат своими руками
На удивление, в русскоязычном сегменте интернета (и на Хабре в том числе) до сих пор крайне мало информации о 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 = '';
var selectedEmotion = iconSelect.getSelectedValue();
if (selectedEmotion !== "") {
emotedText += '';
}
emotedText += message;
if (selectedEmotion !== "") {
emotedText += ' ';
}
emotedText += ' ';
jsonMessage = {
"text": emotedText
};
publishConfig = {
channel: textToSpeechChannel,
message: jsonMessage
};
pubnub.publish(publishConfig);
}
Сначала мы формируем само сообщение, затем определяем конфигурацию, которую понимает SDK, и только после этого инициируем отправку. Дальше лучше. Чтобы превратить текст в синтезированную речь, еще одно сообщение мы отправляем в канал IBM Watson. Для определения эмоциональной окраски используется Speech Synthesis Markup Language (SSML), а если конкретнее — тэг
Среди уже имеющихся на рынке продуктов, использующих возможности PubNub, можно отметить, скажем, концепцию умного дома от Insteon или мобильное приложение для планирования семейных мероприятий от Curago. В завершении еще раз напомню, что полный код примера можно найти на GitHub.