Real-time messaging with Nexmo and Laravel
Published on by Michael Heap
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@betacomposer 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
)
- An expiry time of now + 1 hour (
- 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_id
and 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 focusedreplyInput.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 messagelet typingIndicator = $("<div>"); // Someone's typing, show the typingIndicatorconversation.on("text:typing:on", data => { typingIndicator.text(data.user.name + " is typing..."); replyInput.after(typingIndicator);}); // They stopped typing, remove the typingIndicatorconversation.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:
- Make a
POST
request to ourTicketEntry
endpoint each time we add a new message and persist them in the database - 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.php
now 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.
Michael is a PHP developer advocate at Nexmo. Working with a variety of languages and tools, he shares his technical expertise to audiences all around the world at user groups and conferences. When he finds time to code, he enjoys reducing complexity in systems and making them more predictable.