使用 Nexmo 和 Laravel 实现实时消息
发布日期:作者: Michael Heap
欢迎回到 Deskmo 系列的第三部分,也是(目前)最后一部分。在 第一部分 中,我们构建了一个支持通过 SMS 发送和接收消息的帮助台应用程序。 第二部分 添加了对语音通话的支持,包括文本转语音和转录功能。
今天,我们将使用 Nexmo Stitch 添加应用内消息。Stitch 负责所有实时聊天的繁重工作,为您提供一个 WebSocket,您可以连接到它并监听与您的应用程序相关的事件。虽然我们今天使用 JavaScript,但 Stitch 在 Web、iOS 和 Android 上运行,这意味着您可以使用一项服务在三个不同的平台上支持实时消息!
先决条件
要完成本文的步骤,您需要一个 Nexmo 帐户和本系列第二部分的 Deskmo 应用程序,以及您通常的 Laravel 先决条件。
由于 Stitch 目前处于 开发者预览版,您需要安装 Nexmo CLI 和 Nexmo PHP 客户端的 beta
版本。在与 composer.json
相同的目录中运行以下命令来安装它们
npm install -g nexmo-cli@betacomposer require nexmo/client:1.3.0-beta5
我们还需要在终端中运行 php artisan serve
来启动我们的 Laravel 应用程序。
创建 Nexmo 用户
要使用 Stitch 的应用内消息,我们需要为每个用户创建一个 Nexmo 用户配置文件。为了确保所有用户都拥有 Nexmo 用户配置文件,我们将在注册流程中为每个用户创建配置文件。
需要注意的是,Nexmo 用户配置文件是一个概念,它可以帮助您通过将 Nexmo 配置文件链接到您的本地用户配置文件来跟踪您的用户。这不会为您的用户创建 Nexmo 帐户。
为此,我们需要编辑我们的 users
表以包含一个 nexmo_id
列,并更新 Auth\RegisterController@create
方法,以向 Nexmo 发出 API 调用并存储返回的 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
数组中。这将允许我们在 RegisterController
中创建用户时传递 nexmo_id
。
最后,是时候更新 app/Http/Controllers/Auth/RegisterController.php
来向 Nexmo 发出请求了。让我们从在文件顶部导入所有需要的类开始
use Nexmo;use Nexmo\User\User as NexmoUser;
编辑 create
方法,创建一个新的 NexmoUser
对象,并使用 Nexmo::User()->create()
将用户发送到 Nexmo - 我们使用客户的电子邮件作为其配置文件名称,因为它保证是唯一的,而他们的真实姓名则不一定是唯一的。发出请求后,最后要做的就是在 User::create
语句中添加一行来保存 ID。
$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
来销毁并重新创建我们的数据库。运行该命令后,访问 注册页面 并创建一个新帐户,以创建一个也具有 Nexmo ID 的 Deskmo 用户。此用户将是我们的帮助台代理(用户 ID 1)。您还需要创建一个第二个帐户,它将充当我们的客户(用户 ID 2)。
将应用内消息添加为通知方法
现在我们已经为用户创建了 Nexmo 配置文件,是时候更新工单创建流程,以允许代理选择应用内消息作为通知方法了。更新 resources/views/ticket/create.blade.php
以在我们的 voice
选项下方添加一个新的单选按钮
<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
块,该块检查代理是否选择了 in-app-messaging
作为通知方法(它将位于大约第 100 行)。
} elseif ($data['notification_method'] === 'in-app-messaging') { // Trigger In-App Messaging} else { throw new \Exception('Invalid notification method provided');}
我们的帮助台代理现在可以选择应用内消息作为通知方法,但它实际上不会做任何事情。要开始对话,我们需要告诉 Nexmo 我们希望在创建工单时指定的代理和用户之间进行对话。
我们从在文件顶部导入要使用的类开始
use Nexmo\Conversations\Conversation;
然后,我们可以创建对话并将我们的用户添加为参与者
} 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]);}
此时,我们有一个对话,并且我们的两个用户被添加为参与者。我们需要在构建聊天界面时引用此对话,因此让我们将其存储在刚创建的 Ticket
中的数据库中。
我们需要创建一个迁移以添加一个名为 conversation_id
的新 nullable
字段。该字段是 nullable
,因为并非每个工单都支持应用内消息。
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
以将对话 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>
,以使内容排列整齐。如果刷新工单列表,您将在任何使用应用内消息的工单旁边看到 Live
一词。
安装 Stitch SDK
我们已经完成了所有后台工作,使我们能够使用应用内消息,但我们的用户仍然无法执行任何操作。让我们现在通过在我们的应用程序中添加一个实时聊天框来解决这个问题!Nexmo 通过 NPM 提供了一个预先构建的 SDK,您可以将其集成到您的应用程序中,该 SDK 为您完成了大部分工作
npm install nexmo-conversation --save-dev
这会将库安装到我们的 node_modules
文件夹中,该文件夹无法被我们的应用程序访问。我们需要更新我们的 Laravel Mix 配置以将该文件复制到 public/js
中。编辑 webpack.mix.js
并添加 conversationClient.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');
我们将在下一节中更改我们的 Javascript,因此让我们通过在新的终端窗口中运行 npm run watch
来设置项目的自动重建。
最后,我们需要将 conversationClient.js
包含在我们的 HTML 页面中。编辑 resources/views/layouts/app.blade.php
,并在包含 js/app.js
之前添加 <script src="{{ asset('js/conversationClient.js') }}"></script>
。
创建聊天界面
为了创建我们的聊天室,我们需要编辑resources/views/ticket/show.blade.php
和app/Http/Controllers/TicketController.php
。让我们从填充TicketController::show
方法开始,该方法包含连接到我们对话所需的所有信息。之前我们传入$ticket
,因为这是我们唯一需要的,但要连接到Stitch,我们还需要从页面读取JSON Web Token (JWT)和对话 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
)。 - JWT 有效的路径。在本例中,我们需要使用会话、用户和对话 (
acl
)。
- 当前时间加 1 小时的过期时间 (
- 最后,传递
conversation_id
。
生成的JWT将为所有用户提供对这些路径的无限访问权限。在现实世界中,我们希望限制用户的访问权限,以便他们无法使用JWT向对话中添加/删除用户。
这是Nexmo SDK连接到Stitch并开始发送消息所需的所有信息。
除了连接到API,我们还需要提供一个界面,以便通过我们的应用程序添加新的回复。打开resources/views/ticket/show.blade.php
,并在.panel-body
的结束div
标签后添加以下内容,以创建一个表单,我们可以用它添加回复。
@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
除了创建表单,我们还必须使user_jwt
、conversation_id
和ticket_id
ID对Nexmo SDK可访问。为此,请在@endsection
行之前添加以下内容,以创建三个稍后可以读取的JavaScript常量。
<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; }); }); });}
这是使应用程序内消息传递正常工作所需的所有JavaScript代码!保存文件,然后打开一个使用应用程序内消息传递作为通知方法的工单进行测试。您在文本区域中输入的任何内容都将通过WebSocket发送到Nexmo,然后分发到所有其他连接。通过打开另一个浏览器、以第二个用户的身份登录并浏览相同的工单来进行测试。您在一个窗口中添加的任何内容都会立即显示在另一个窗口中。
现在我们已经实现了应用程序内聊天功能,是时候添加一些使界面更易于使用的细节,例如“有人正在输入”指示器。Nexmo也会为您处理所有这些内容 - 您只需要在正确的时间触发正确的事件。我们假设当文本输入处于焦点状态时,有人正在输入,并且当文本输入失去焦点时,他们停止输入。要启用此功能,请在$("#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();});
这只是Nexmo可以处理的实用程序事件的一个示例。当成员加入和离开对话时,当消息被看到时,以及当对话详细信息被更新时,都会有事件。您可以在Stitch文档中了解更多有关可用事件的信息。如前所述,Stitch目前处于开发者预览阶段,因此如果您有兴趣了解更多有关可用事件的信息,请随时加入Nexmo社区Slack频道,与我们聊聊。
持久化回复
您可能已经注意到,我们的应用程序内消息传递解决方案存在一个主要问题 - 当我们刷新页面时,我们的对话会消失!谢天谢地,我们有两个选项可以帮助解决这个问题。
- 每次添加新消息时,向我们的
TicketEntry
端点发送一个POST
请求,并将它们持久化到数据库中。 - 使用Nexmo SDK中的
conversation.getEvents()
方法列出对话中到目前为止的所有事件,并在页面加载时重建应用程序状态。
由于我们已经将SMS和语音回复存储在数据库中,因此我们将使用选项#1。要保存请求,我们需要再次编辑app.js
。Laravel捆绑了axios
,所以让我们用它向/ticket-entry
发送一个包含发送者的nexmo_id
、消息text
和当前ticket_id
的HTTP请求。将其直接添加到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,但目前该端点只适合来自Nexmo的入站SMS请求。现在让我们编辑app/Http/Controllers/TicketEntryController.php
,并在store
方法中添加对应用程序内消息的支持。
首先需要更改的是我们的验证。我们之前期望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']);
接下来,我们需要根据是否有用户的phone_number
或nexmo_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';}
由于我们使用的是Ticket
模型,因此我们也需要在文件顶部导入它。
use App\Ticket;
一旦处理完毕,我们只需要更新我们的TicketEntry
以使用$channel
变量,而不是硬编码sms
。
$entry = new TicketEntry([ 'content' => $data['text'], 'channel' => $channel,]);
我们的端点现在将支持入站SMS消息和应用程序内消息。通过发送应用程序内消息来试一试,观察它如何在所有浏览器中同时出现,然后尝试刷新页面以查看从数据库中填充的所有条目。
结论
在这篇文章中,我们在不到100行代码中将实时消息传递功能添加到了我们的应用程序中。这要归功于Stitch,它为我们处理了核心功能,例如发送消息,以及您的存在指示器。如果您想了解更多关于Stitch的信息,请查看Nexmo开发者网站上的Stitch文档。
遗憾的是,这将我们带到了Deskmo系列的结尾(目前!)。总之,我们构建了一个帮助台系统,它允许您使用SMS、语音或应用程序内消息与客户进行通信。如果您想自己尝试运行它,可以在Github上找到最终代码。
如果您想要一些Nexmo积分来完成这篇文章并测试平台,请通过[email protected]联系我们,并引用LaravelNews,我们会为您解决这个问题。
如果您有任何想法或问题,请随时联系Twitter上的@mheap或[email protected]。
非常感谢Nexmo本周赞助Laravel News。
Michael是Nexmo的PHP开发者倡导者。他使用各种语言和工具,在用户组和会议上向世界各地的观众分享他的技术专长。当他找到时间编写代码时,他喜欢降低系统复杂性并使它们更具可预测性。