Real-time messaging with Nexmo and Laravel

Published on by

Real-time messaging with Nexmo and Laravel image

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.

Michael Heap photo

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.

Cube

Laravel Newsletter

Join 40k+ other developers and never miss out on new tips, tutorials, and more.

image
Laravel Forge

Easily create and manage your servers and deploy your Laravel applications in seconds.

Visit Laravel Forge
Laravel Forge logo

Laravel Forge

Easily create and manage your servers and deploy your Laravel applications in seconds.

Laravel Forge
Tinkerwell logo

Tinkerwell

The must-have code runner for Laravel developers. Tinker with AI, autocompletion and instant feedback on local and production environments.

Tinkerwell
No Compromises logo

No Compromises

Joel and Aaron, the two seasoned devs from the No Compromises podcast, are now available to hire for your Laravel project. ⬧ Flat rate of $7500/mo. ⬧ No lengthy sales process. ⬧ No contracts. ⬧ 100% money back guarantee.

No Compromises
Kirschbaum logo

Kirschbaum

Providing innovation and stability to ensure your web application succeeds.

Kirschbaum
Shift logo

Shift

Running an old Laravel version? Instant, automated Laravel upgrades and code modernization to keep your applications fresh.

Shift
Bacancy logo

Bacancy

Supercharge your project with a seasoned Laravel developer with 4-6 years of experience for just $2500/month. Get 160 hours of dedicated expertise & a risk-free 15-day trial. Schedule a call now!

Bacancy
LoadForge logo

LoadForge

Easy, affordable load testing and stress tests for websites, APIs and databases.

LoadForge
Paragraph logo

Paragraph

Manage your Laravel app as if it was a CMS – edit any text on any page or in any email without touching Blade or language files.

Paragraph
Lucky Media logo

Lucky Media

Bespoke software solutions built for your business. We ♥ Laravel

Lucky Media
Lunar: Laravel E-Commerce logo

Lunar: Laravel E-Commerce

E-Commerce for Laravel. An open-source package that brings the power of modern headless e-commerce functionality to Laravel.

Lunar: Laravel E-Commerce
DocuWriter.ai logo

DocuWriter.ai

Save hours of manually writing Code Documentation, Comments & DocBlocks, Test suites and Refactoring.

DocuWriter.ai
Rector logo

Rector

Your partner for seamless Laravel upgrades, cutting costs, and accelerating innovation for successful companies

Rector

The latest

View all →
Generate Code Coverage in Laravel With PCOV image

Generate Code Coverage in Laravel With PCOV

Read article
Non-backed Enums in Database Queries and a withSchedule() bootstrap method in Laravel 11.1 image

Non-backed Enums in Database Queries and a withSchedule() bootstrap method in Laravel 11.1

Read article
Laravel Pint --bail Flag image

Laravel Pint --bail Flag

Read article
Laravel Herd for Windows is now released! image

Laravel Herd for Windows is now released!

Read article
The Laravel Worldwide Meetup is Today image

The Laravel Worldwide Meetup is Today

Read article
Cache Routes with Cloudflare in Laravel image

Cache Routes with Cloudflare in Laravel

Read article