Join 26,000+ Laravel Developers and join the free Laravel Newsletter
Real-time messaging with Nexmo and Laravel
Laravel Tutorials / updated: March 08, 2018

Real-time messaging with Nexmo and Laravel

Welcome back to the third and final (for now!) part of the Deskmo series. In part 1 we built a help desk application with support for sending and receiving messages via SMS. Part 2 added support for voice calls, with text-to-speech and transcription support.

Today, we’re going to add in-app messaging using Nexmo Stitch. Stitch takes care of all of the heavy lifting for real-time chat, providing you with a websocket that you connect to and listen for events relating to your application. Whilst we’re using JavaScript today, Stitch works on the web, iOS and Android which means you can support real-time messaging across three different platforms with a single service!

Prerequisites

To work through this post, you’ll need a Nexmo account and the Deskmo application from part 2 of this series, as well as your usual Laravel prerequisites.

As Stitch is currently in Developer Preview you’ll need to install a beta version of both the Nexmo CLI and the Nexmo PHP client. Run the following in the same directory as composer.json to install them both:

npm install -g nexmo-cli@beta
composer require nexmo/client:1.3.0-beta5

We’ll also need to run php artisan serve in the terminal to start serving our Laravel application.

Creating a Nexmo User

To use in-app messaging with Stitch, we need to create a Nexmo user profile for each of our users. To ensure that all of our users have a Nexmo user profile, we’re going to create a profile for each user as part of our registration flow.

It’s important to note that a Nexmo user profile is a concept that helps you keep track of your users by linking a Nexmo profile to your local user profile. This does not create a Nexmo account for your users

To do this, we need to edit our users table to contain a nexmo_id column, and update the Auth\RegisterController@create method to make an API call to Nexmo and store the returned ID. Let’s start by creating the migration – run php artisan make:migration add_nexmo_id_to_users and populate the file that is created with the following contents:

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');
    });
}

Next, we need to add the nexmo_id property to our User model. Open up app/User.php and add nexmo_id to the $fillable array at the top of the file. This will allow us to pass in a nexmo_id when we create a user in the RegisterController.

Finally, it’s time to update app/Http/Controllers/Auth/RegisterController.php to make a request to Nexmo. Let’s start by importing all of the classes we’ll need at the top of the file:

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

Edit the create method, create a new NexmoUser object and use Nexmo::User()->create() to send the user to Nexmo – we’re using the customer’s email as their profile name as it is guaranteed to be unique, whilst their real name is not. Once we’ve made that request, the last thing to do is save the ID by adding a line to our User::create statement.

$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(),
]);

Careful! In the next paragraph we run migrate:refresh which will reset your database. We need to do this, so back up any data you may want to keep.

From this point forwards any new users in our application will also be registered with Nexmo. Let’s give that a go now by running php artisan migrate:refresh to destroy and recreate our database. After running that, visit the registration page and create a new account to create a Deskmo user that also has a Nexmo ID. This user will be our help desk agent (user ID 1). You’ll also need to create a second account that will fill the role of our customer (user ID 2).

Adding in-app messaging as a notification method

Now that we’ve created a Nexmo profile for our user, it’s time to update the ticket creation flow to allow the agent to choose in-app messaging as a notification method. Update resources/views/ticket/create.blade.php to add a new radio button underneath our voice option:

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

Once we’ve done that, we need to update app/Http/Controllers/TicketController.php to add another elseif block that checks if the agent has chosen in-app-messaging as the notification method (it’ll be around line 100):

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

Our help desk agent can now select in-app messaging as the notification method, but it won’t actually do anything. To start a conversation we need to tell Nexmo that we’d like a conversation between our agent and the user specified when we created the ticket.

We start by important the classes we’re going to use at the top of the file:

use Nexmo\Conversations\Conversation;

Then, we can create a conversation and add our users to it as participants:

} 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]);
}

At this point, we have a conversation and our two users added as participants. We’ll need to refer to this conversation later on when we’re building our chat interface, so let’s store in the database against the Ticket we just created.

We need to create a migration to add a new nullable field named conversation_id. The field is nullable as not every ticket will support in-app messaging:

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');
    });
}

Run php artisan migrate to create the column, then edit app/Http/Controllers/TicketController.php again to save the conversation ID to our ticket by adding the following code after $conversation->addMember($users[$cc->user->nexmo_id]);:

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

The final thing to do is add a small notification to our list of tickets if in-app messaging is available. Open up resources/views/ticket/index.blade.php and add the following cell to the table:

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

You’ll also want to add a <th> to the heading to make things line up correctly. If you refresh your list of tickets you will see the word Live next to any tickets using in-app messaging.

Installing the Stitch SDK

We’ve completed all of the background work to enable us to use in-app messaging, but our users still can’t do anything. Let’s fix that now by adding a real-time chat box to our application! Nexmo provide a prebuilt SDK via NPM that you can integrate in to your application that does most of the work for you:

npm install nexmo-conversation --save-dev

This installs the library in to our node_modules folder, which isn’t accessible by our application. We need to update our Laravel Mix configuration to copy the file in to public/js. Edit webpack.mix.js and add conversationClient.js so that the file looks like the following:

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');

We’re going to be changing our Javascript in the next section, so let’s set up automatic rebuilding of our project by running npm run watch in a new terminal window.

Finally, we need to include conversationClient.js in our HTML page. Edit resources/views/layouts/app.blade.php and add <script src="{{ asset('js/conversationClient.js') }}"></script> before we include js/app.js.

Creating a chat interface

To create our chat room, we need to edit resources/views/ticket/show.blade.php andapp/Http/Controllers/TicketController.php. Let’s start by populating the TicketController::show method with all of the information we’ll need to connect to our conversation. Previously we passed in the $ticket as that was all we needed, but to connect to Stitch we’ll also need to read a JSON Web Token (JWT) and a conversation ID from the page.

This is going to look a little confusing if you’re not familiar with JWTs, so I’ll show you the entire thing to start with and then walk through it line by line

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,
]);

This code does the following:

  • Pass through the ticket information
  • Generate a JWT (user_jwt) containing:
    • An expiry time of now + 1 hour (exp)
    • The user to authenticate as (sub)
    • The paths the JWT is valid for. In this case, we need to work with sessions, users and conversations (acl)
  • Finally, pass through the conversation_id)

The JWT generated will provide all users unlimited access to those paths. In the real world, we’d want to restrict the access that our users had so that they couldn’t add/remove users from a conversation using the JWT

This is all the information that we need for the Nexmo SDK to connect to Stitch and start sending messages.

As well as connecting to the API, we need to provide an interface to add new replies via our application. Open resources/views/ticket/show.blade.php and the following after the closing div tag of .panel-body to create a form that we can use to add a reply.

@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

In addition to creating a form, we also have to make our user_jwt, conversation_idand ticket_id ID accessible to the Nexmo SDK. To do this, add the following just the @endsection line to create three JavaScript constants that we can read later.

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

We’re all done with the HTML for our application! There’s only one thing left to do, and that’s write a little bit of JavaScript to glue everything together. Open up resources/assets/js/app.js and replace everything that’s in there with the following:

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;
            });
        });
    });
}

This is all the JavaScript you need for in-app messaging to work! Save the file and then open up a ticket with in-app messaging as the notification method to test it out. Anything you type into the textarea will be sent over a WebSocket to Nexmo, then distributed to all other connections. Test it out by opening another browser, logged in as your second user and browsing to the same ticket. Any content that you add in one window will instantly appear in the other.

Now that we have in-app chat working, it’s time to add some of the details that make an interface nicer to use, such as a “Someone is typing” indicator. Nexmo handle all of this for you too – you just need to fire the correct events at the correct time. We’ll assume that someone is typing when the text input is focused, and that they’ve stopped typing when it is not focused. To enable this feature, add the following after $("#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();
});

This is just one example of the utility events that Nexmo can handle. There are events for when members join and leave conversations, when messages are seen and when conversation details are updated. You can learn more about the available events in the Stitch documentation. As mentioned, Stitch is currently in Developer Preview so if you’re interested in learning more about the available events feel free to join the Nexmo community Slack channel to chat with us about it.

Persisting the responses

You may have noticed that there’s one major issue with our in-app messaging solution – when we refresh the page our conversation disappears! Thankfully, we have two options to help solve this problem:

  1. Make a POST request to our TicketEntry endpoint each time we add a new message and persist them in the database
  2. Use the conversation.getEvents() method in the Nexmo SDK to list all events so far in a conversation and rebuild the application state when the page loads

As we’re already storing our SMS and voice replies in the database, we’ll go with option #1. To save the requests, we need to edit app.js again. Laravel comes bundled with axios, so let’s use that to make a HTTP request to /ticket-entry containing the nexmo_id of the sender, the message text and the current ticket_id. Add it directly after 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
    });

This takes care of sending the data to our API, but at the moment that endpoint is only suitable for the inbound SMS requests from Nexmo. Let’s edit app/Http/Controllers/TicketEntryController.phpnow and add support for in-app messages in the store method.

The first thing that needs to change is our validation. We previously expected an msisdn as our identifier, but now we expect either an msisdn or a nexmo_id. Laravel provides the required_without_all validator to help us achieve this:

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

Next, we need to load the user depending on if we have their phone_number or nexmo_id. Whilst we’re here, we’ll also either grab the latest ticket or use what we were sent in the request. Then finally, we’ll set the channel the message was received on:

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';
}

As we’re using the Ticket model, we also need to import that at the top of our file:

use App\Ticket;

Once that’s taken care of, we just need to update our TicketEntry to use the $channel variable rather than hard coding sms:

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

Our endpoint will now support inbound SMS messages and in-app messages. Give it a go by sending an in-app message, watch as it appears in all browsers at the same time and then try refreshing the page to see all of the entries populated from our database

Conclusion

In this post, we added real-time messaging to our application in under 100 lines of code. This is thanks to Stitch, which handles the core feature like sending messages, as well as presence indicators for you. If you’d like to learn more about Stitch, take a look at the Stitch documentation on Nexmo Developer.

Sadly, this brings us to the end of the Deskmo series (for now!). Together, we’ve built up a help desk system that allows you to communicate with your customers using either SMS, Voice or In-App messaging. You can find the final code on Github if you want to try running it yourself.

If you’d like some Nexmo credit to work through this post and test the platform out, get in touch at devrel@nexmo.com quoting LaravelNews and we’ll get that sorted for you.

If you have any thoughts or questions, don’t hesitate to reach out to @mheap on Twitter or to devrel@nexmo.com.


Many thanks for Nexmo sponsoring Laravel News this week.

Laravel News Partners

Newsletter

Join the weekly newsletter and never miss out on new tips, tutorials, and more.