Создаем онлайн (real time) чат используя Laravel и Nexmo

Вот и настало время 3й части об интеграции Laravel и Nexmo. В первых двух частях вы научились отправлять SMS и совершать голосовые звонки используя Nexmo.

Сегодня мы расширим функционал и добавим онлайн чат используя Nexmo Stitch. Stitch - возьмет на себя заботу о всем том, что необходимо для создания real-time чата, предоставляя нам websocket к которому мы будем подключаться и получать события для нашего чата. Сегодня мы используем только Javascript api, однако Stitch может так же работать на iOS и Android, так что вы с легкостью сможете добавить поддержку этих платформ в свой проект.

Перед началом работы

Перед тем как вы начнете чтение данной статьи, вам необходимо иметь аккаунт Nexmo, а так же загрузить код Deskmo из github, если вы пропустили первые две статьи.

Nexmo Stitch на данный момент находится на стадии Developer Preview, так что вам необходимо установить бета версии Nexmo CLI и Nexmo PHP. Выполните следующие команды в той же директории где находится ваш composer.json:

npm install -g [email protected]
composer require nexmo/client:1.3.0-beta5

А так же выполните команду php artisan serve чтобы запустить вебсервер.

Создание пользователя в Nexmo

Нам потребуется создать персональный профиль для каждого пользователя, для того чтобы использовать Nexmo Stitch. Чтобы на 100% быть уверенными что у каждого нашего пользователя есть профиль в Nexmo, мы будем создавать такой профиль во время регистрации.

На заметку: профиль в Nexmo - это не тоже самое что аккаунт в Nexmo. Профиль необходим чтобы вы могли связать пользователя в Nexmo с вашим локальным пользователем. Создание профиля НЕ создает аккаунт в Nexmo.

Сперва нам необходимо изменить таблицу users, и добавить в нее поле nexmo_id. а затем добавить в метод Auth\[email protected] API запрос, который будет создавать профиль в Nexmo и возвращать ID. Начнем с создания миграции. Выполните команду:

php artisan make:migration add_nexmo_id_to_users

И измените созданный файл миграции следующим образом:

public function up()
{
  Schema::table('users', function (Blueprint $table) {
    $table->string('nexmo_id');
  });
}

public function down()
{
  Schema::table('users', function (Blueprint $table) {
    $table->dropColumn('nexmo_id');
  });
}

Далее мы добавим поле nexmo_id в модель User. Откройте файл app/User.php и добавьте nexmo_id в массив $fillable. Это позволит передавать nexmo_id в метод create, во время регистрации в RegisterController.

И наконец мы можем изменить app/Http/Controllers/Auth/RegisterController.php и добавить в него API вызов, который будет создавать профиль в Nexmo. Добавьте в начале файла строки:

use Nexmo;
use Nexmo\User\User as NexmoUser;

Теперь измените метод create, добавив в него создание нового объекта NexmoUser и отправку запроса с помощью Nexmo::User()->create(). Мы будем использовать email пользователя в качестве его имени в Nexmo:

$user = (new NexmoUser())->setName($data['email']);
$nexmoUser = Nexmo::user()->create($user);

return User::create([
  'name' => $data['name'],
  'email' => $data['email'],
  'password' => bcrypt($data['password']),
  'phone_number' => $data['phone_number'],
  'nexmo_id' => $nexmoUser->getId(),
]);

Осторожно! Команда migrate:refresh сотрет все данные из вашей базы данных. Если у вас там есть что-то нужно, то советую сделать бекап, перед тем как выполнять эту команду.

С этого момента все новые пользователи которые будут регистрироваться в нашей системе, будут так же автоматически зарегистрированны в Nexmo. Выполните команду php artisan migrate:refresh чтобы пересоздать базу данных. После этого перейдите на страницу регистрации и создайте двух новых пользователей (менеджера - role id = 1 и кастомера - role id = 2).

Добавление чата как метод нотификации

Настало время изменить процесс создания заявки, и добавить в него возможность выбрать веб-чат как способ для связи. Откройте файл resources/views/ticket/create.blade.php и добавьте в него код ниже:

<div class="radio">
  <label>
    <input type="radio" name="notification_method" value="in-app-messaging">
    In-App Messaging
  </label>
</div>

Затем откройте app/Http/Controllers/TicketController.php и добавьте еще один elseif блок (где-то в районе 100 строчки):

} elseif ($data['notification_method'] === 'in-app-messaging') {
  // Trigger In-App Messaging
} else {
  throw new \Exception('Invalid notification method provided');
}

Теперь менеджер может выбрать веб-чат как способ связи, но пока что этот способ не работает. Чтобы он заработал, нам нужно сказать Nexmo, что мы хотим создать диалог между менеджером и пользователем.

Все в том же файле TicketController.php добавьт в начало:

use Nexmo\Conversations\Conversation;

И замените добавленный ранее elseif на следующий код:

} elseif ($data['notification_method'] === 'in-app-messaging') {
  $conversation = (new Conversation())->setDisplayName('Ticket '.$ticket->id);
  $conversation = Nexmo::conversation()->create($conversation);

  // Add the users to the conversation
  $users = Nexmo::user();
  $conversation->addMember($users[$user->nexmo_id]);
  $conversation->addMember($users[$cc->user->nexmo_id]);
}

Тут мы создаем диалог в Nexmo, и добавляем в него наших пользователей. Этот диалог понадобится нам позднее, когда мы будем делать веб-часть, а пока просто сохраним его в моделе Ticket.

Создадим миграцию для таблицы tickets, и добавим в нее новое поле conversation_id.

php artisan make:migration add_conversation_id_to_ticket

public function up()
{
  Schema::table('tickets', function (Blueprint $table) {
    $table->string('conversation_id')->nullable();
  });
}

public function down()
{
  Schema::table('tickets', function (Blueprint $table) {
    $table->dropColumn('conversation_id');
  });
}

Выполните команду php artisan migrate чтобы применить изменения, а затем вновь откройте app/Http/Controllers/TicketController.php и добавьте сохранение conversation_id (сразу после строчки $conversation->addMember($users[$cc->user->nexmo_id]);):

$ticket->conversation_id = $conversation->getId();
$ticket->save();

И заключительная деталь, добавим индикацию для заявки доступен онлайн чат. Откройте файл resources/views/ticket/index.blade.php и добавьте ячейку в таблицу:

<td>{{ $ticket->conversation_id ? "Live" : "" }}</td>

Не забудьте добавить еще один тег th, чтобы таблица не съезжала.

Установка Stitch SDK

Мы закончили подготовительную работу на бекенде, настало время для фронта. Nexmo предоставляет нам SDK, которое сделает всю работу за нас. Давайте же его установим:

npm install nexmo-conversation --save-dev

Изменим файл webpack.mix.js чтобы он копировал SDK из папки node_modules в папку public/js:

mix.js('resources/assets/js/app.js', 'public/js')
  .js('node_modules/nexmo-conversation/dist/conversationClient.js', 'public/js')
  .sass('resources/assets/sass/app.scss', 'public/css');

Далее мы будем работать с JS кодом, поэтому стоит запустить команду npm run watch, чтобы наш JS код автоматически пересобирался при изменениях.

Не забудем подключить conversationClient.js в наш HTML. Откройте файл resources/views/layouts/app.blade.php и добавьте код:

<script src="{{ asset('js/conversationClient.js') }}"></script>

прямо перед подкючением js/app.js.

Интерфейс для Чата

Для создания интерфейса нам потребуется внести изменения в файлы resources/views/ticket/show.blade.php и app/Http/Controllers/TicketController.php. Начнем с того, что добавим в метод TicketController::show всю необходимую информацию для подключения нашего диалога. Ранее мы передавали только $ticket объект, но теперь нам так же потребуется передавать JSON Web Token (JWT) и conversation ID.

Если вы не знакомы с концепцией JWT - ничего страшного. Для начала я приведу весь код, а далее пройдемся по каждой сточке:

return view('ticket.show', [
  'ticket' => $ticket,
  'user_jwt' => Nexmo::generateJwt([
    'exp' => time() + 3600,
    'sub' => Auth::user()->email,
    'acl' => ["paths" => ["/v1/sessions/**" => (object)[], "/v1/users/**" => (object)[], "/v1/conversations/**" => (object)[]]],
  ]),
  'conversation_id' => $ticket->conversation_id,
]);

Что тут происходит:

  • Передаем информацию по заявке ($ticket)
  • Создаем JWT (user_jwt):
    • Время жизни токена - 1 час (exp)
    • Пользователь которого мы авторизовываем (sub)
    • Пути для которых данный токен считается валидным. (acl)
  • ID диалога (conversation_id)

Это все что нам понадобится чтобы подключиться к Stitch.

Кроме прочего, нам так же необходимо добавить html код для добавления ответов. Откройте файл resources/views/ticket/show.blade.php и добавьте следующий код сразу после закрывающего тега div с классом .panel-body:

@if ($conversation_id)
<div class="panel-body">
  <form action="" method="POST" id="add-reply">
    <div class="form-group">
      <label for="reply">Add a reply</label>
      <textarea class="form-control" id="reply" rows="3"></textarea>
    </div>
    <button type="submit" class="btn btn-primary mb-2" style="display:none;" id="reply-submit">Save</button>
  </form>
</div>
@endif

В дополнение к форме, нам необходимо чтобы Nexmo SDK мог получить доступ к переменным user_jwt, conversation_id и ticket_id. Добавьте код ниже, прямо перед строчкой с @endsection:

<script>
  const USER_JWT = '{{$user_jwt}}';
  const CONVERSATION_ID = '{{$conversation_id}}';
  const TICKET_ID = '{{$ticket->id}}';
</script>

На этом мы закончили с HTML частью, и теперь нам осталась только написать Javascript код, который свяжет все воедино. Откройте файл resources/assets/js/app.js и замените все что там есть, кодом ниже:

require('./bootstrap');

if (typeof CONVERSATION_ID !== "undefined" && CONVERSATION_ID !== "") {
  var replyInput = $("#reply");
  // Use the JWT we defined to log in to Stitch
  new ConversationClient({debug: false}).login(USER_JWT).then(app => {
    // Connect to the conversation using the ID we provided earlier
    app.getConversation(CONVERSATION_ID).then((conversation) => {
      // Once the conversation is loaded, show the submit button
       $("#reply-submit").show();

      // Add an event listener so that whenever we receive a `text` event
      // we add the text to our list of responses
      conversation.on('text', (sender, message) => {
        $(".panel-body:first").append("<strong>" + sender.user.name + " / web / In-App Message</strong><p>"+message.body.text+"</p><hr />");
      })

      // Add a listener to the form and prevent it submitting via HTTP POST
      // Instead, send it via the Nexmo SDK using conversation.sendText()
      $("#add-reply").submit(() => {
        conversation.sendText(replyInput.val()).then(console.log).catch(console.log)
        replyInput.val("");
        return false;
      });
    });
  });
}

Это весь Javacript код который нам необходим для начала работы. Сохраните файл и откройте новую заявку с выбранным методом "веб-чат" в качестве способа связи. Все что вы напишите в форме, будет отправленно через websocket в Nexmo, откуда потом будет разосланно всем подписчикам которые подключенены. Попробуйте открыть еще одно окно браузера в инкогнито, и зайдите в нем на ту же страницу. Любой контент который вы добавите в одном окне, должен будет появиться в другом.

Теперь, когда все работает, настало время добавить немного деталей, чтобы сделать интерфейс более удобным для пользователей, например, индикацию что кто-то набирает текст. И даже об этом Nexmo уже позаботился для нас в своем SDK. Все что нам необходимо сделать, это добавить реакцию на соответствующие события. Для простоты предположим, что событие "набирает текст..." будет происходить когда поле textarea получает фокус, и соответсвенно когда это поле теряет фокус, пользователь перестал набирать текст. Добавьте этот код сразу после строчки $("#reply-submit").show();:

// We assume they're typing when the input is focused
replyInput.focus(() => conversation.startTyping().then(console.log).catch(console.log));
replyInput.blur(() => conversation.stopTyping().then(console.log).catch(console.log));

// Create an element to hold our typing message
let typingIndicator = $("<div>");

// Someone's typing, show the typingIndicator
conversation.on("text:typing:on", data => {
typingIndicator.text(data.user.name + " is typing...");
replyInput.after(typingIndicator);
});

// They stopped typing, remove the typingIndicator
conversation.on("text:typing:off", data => {
typingIndicator.remove();
});

Это лишь одно из событий которое Nexmo SDK может обрабатывать. Так же вам доступны событие: пользователь присоединился к чату, пользователь покинул чат, и даже событие когда пользователь увидел сообщение.

Сохранение переписки

Должно быть вы заметили, что если вы обновите страницу, то вся переписка исчезнет. Если вы хотите сохранить переписку, у вас есть два варианта:

  • Делать POST запрос к нашему серверу, каждый раз когда приходит новое сообщение и сохранять его в базе данных.
  • Использовать метод conversation.getEvents() из Nexmo SDK, чтобы получить всю историю для конкретного диалога, и вывести ее на экран.

Так как мы уже сораняем SMS и голосовые ответы в базе данных, нам не составит труда добавить сохранение переписки для чата. Откройте файл app.js и сразу после строчки conversation.on('text') добавьте следующий код:

conversation.on('text', (sender, message) => {
  axios.post('/ticket-entry', {
    "nexmo_id": sender.user.id,
    "text": message.body.text,
    "ticket_id": TICKET_ID
  });

Этот код отправляет запрос на наш API с помощью библиотеки axios (которая идет в стандартной поставке Laravel).

На данный момент API /ticket-entry не умеет сохранять сообщения из чата, так что нам нужно научить его делать это. Откройте файл app/Http/Controllers/TicketEntryController.php.

Сперва нам необходимо изменить правила валидации. Ранее нам требовалось проверять наличие поля msisdn, однако сейчас нам необходимо проверять наличие либо поля msisdn, либо поля nexmo_id. Для подобных случаев в Laravel есть правило валидации required_without_all, которое даст нужный нам эффект:

$data = $this->validate($request, [
  'nexmo_id' => 'required_without_all:msisdn',
  'ticket_id' => 'required_without_all:msisdn',
  'msisdn' => 'required_without_all:nexmo_id',
  'text' => 'required'
]);

Далее, нам необходимо загрузить информацию о пользователе либо по его номеру телефона, либо по nexmo_id (в зависимости от того, какое поле передано). Так же мы получим либо последнюю заявку, либо ту, ID которой был передан. И наконец мы укажем канал по которому было переданно сообщение.

if (isset($data['msisdn'])) {
  $user = User::where('phone_number', $data['msisdn'])->firstOrFail();
  $ticket = $user->latestTicketWithActivity();
  $channel = 'sms';
} else {
  $user = User::where('nexmo_id', $data['nexmo_id'])->firstOrFail();
  $ticket = Ticket::findOrFail($data['ticket_id']);
  $channel = 'web';
}

И не забудьте добавить в начале файла импорт use App\Ticket;

Осталось только изменить создание TicketEntry, чтобы канал сообщения брался из переменной $channel:

$entry = new TicketEntry([
  'content' => $data['text'],
  'channel' => $channel,
]);

Теперь все ваши сообщения будут сохраняться в базе данных даже после перезагрузки страницы.

Заключение

В данной статье мы добавили поддержку real time общения в наш сервис с помощью менее чем 100 строк кода. В этом нам очень сильно помог Nexmo Stitch, который взял на себя всю заботу о websocket и большую часть кода.

На этом серия статей про Deskmo подходит к концу. Вместе мы построили систему поддержки пользователей, которая позволяет отправлять и получать сообщения с помощью: SMS, Голоса и онлайн чата. Весь код данного проекта вы можете найти на Github.

Спасибо что прочитали. Это был перевод статьи Real-time messaging with Nexmo and Laravel.

Опубликовано:

Категории: Статьи