Используем PubNub: эмоциональный говорящий чат своими руками

в 7:45, , рубрики: .net, iaas, javascript, pubnub, Программирование, Системы обмена сообщениями

image

На удивление, в русскоязычном сегменте интернета (и на Хабре в том числе) до сих пор крайне мало информации о 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.

Автор: Cromathaar

Источник

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


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