在这篇文章中,你将学习如何用.NET 6开发一个web API来处理Twilio webhooks,并将其部署到AWS Lambda。你还将学习如何将通话录音作为MP3文件保存到AWS S3。
什么是webhooks?
在今天这个API驱动的世界里,整合应用比以往任何时候都要容易。大多数时候,你可以从外部系统的API中获得你所需要的信息,但有时你想在事情发生时得到外部系统的通知。这就是webhooks的作用。你向外部系统注册你自己的端点,当你要找的事件发生时,他们会向你的端点发布数据。
Twilio Webhooks
你能从webhooks得到的数据类型取决于Twilio的服务。对于Twilio语音API,有几种类型的webhooks,在本教程中你将使用其中的三种:
- 来电语音呼叫
- 状态回拨
- 录音状态回拨
来电语音呼叫webhook,顾名思义,就是你处理来电的地方。当你使用Twilio等可编程的语音服务时,这是你想实现的核心功能。如果你不处理来电,当你呼叫你的Twilio号码时,你会听到三声提示音,然后呼叫就终止了。这个呼叫甚至不会出现在日志中。正如你在本文后面所看到的,当你实现一个呼叫处理程序时,你可以向Twilio提供指示,记录呼叫,播放音频,等等。其中一些动作也有自己的后续webhooks。这些指令是用TwiML(Twilio标记语言)实现的。TwiML是一种基于XML的标记语言,它有一些元素,如Say(向来电者阅读文本)、Dial(向通话中添加另一方)和Record(记录来电者的声音)。你以后将在你的项目中使用Say和Record。
呼叫完成后(呼入或呼出),Twilio会向你的端点发送一个HTTP请求。这被称为状态回调。
如果你要求对通话进行录音,你可以收到录音状态的回调。然后,Twilio向你的端点发送一个带有录音状态的消息和一个访问录音文件的URL。你必须指定你的webhook URL来处理这个回拨消息。
默认情况下,录音URL不需要认证,录音也不加密。然而,你可以要求基本认证来访问录音,并在语音设置(语音→设置→常规)中配置录音为加密的。
在本教程中,你将与Twilio语音产品互动,但许多其他产品也使用webhooks,你可以对它们应用与语音相同的技术。
现在你已经了解了这三个webhooks,让我们继续下一节。
设置AWS IAM用户
你将需要凭证来从命令行将你的应用程序部署到AWS。要创建凭证,请按照下面的步骤进行。
首先,进入AWS IAM用户仪表板,点击添加用户按钮。
输入用户名,如twilio-webhook-user,并勾选访问密钥 - 程序化访问复选框:

点击 "下一步",右下方的权限按钮。
然后,选择直接附加现有策略,并选择AdministratorAccess。

点击 "下一步",右下角的标签按钮。标签是可选的(也是相当有价值的信息),为你创建的资源添加描述性的标签是一个好的做法。由于这是一个演示项目,你可以跳过这一步,点击下一步。审查"按钮。
在审查页面上确认你的选择,它应该看起来像这样:

然后,单击 "创建用户"按钮。
在用户创建过程的最后一步,你应该第一次和最后一次看到你的凭证。
在你按下关闭按钮之前,记下你的访问密钥ID和秘密访问密钥。
现在,打开一个终端窗口,运行以下命令:
aws configure
你应该看到一个AWS访问密钥ID的提示。复制并粘贴你的访问密钥ID,然后按回车键。
然后,复制并粘贴你的秘密访问密钥,并按回车键:

当提示时,输入us-east-1作为默认区域名称 ,然后按回车键。
在这个例子中,我将使用us-east-1区域。区域是AWS拥有其数据中心的地理位置。在生产部署中,尽可能地靠近你的客户,以减少延迟,这是一个好的做法。由于这是一个演示项目,为了方便,你可以使用us-east-1,因为它是AWS管理控制台的默认区域。你可以在这个文件中找到更多关于AWS区域的信息。区域和可用区。
作为默认的输出格式,输入json并按回车键。
为了确认你已经正确配置了你的AWS配置文件,请运行以下命令:
aws configure list
输出应该是这样的:

现在你已经设置了你的AWS凭证,你可以继续设置代码了。
为AWS Lambda创建一个ASP.NET Core项目
你可以从GitHub下载完成的项目。不过,本文将提供分步说明,以便自己进行设置。
打开一个终端,导航到将成为你的项目根的目录。
你将在示例项目中使用Lambda ASP.NET Core Web API项目模板。因此,首先,通过运行以下命令来安装Lambda模板:
dotnet new -i Amazon.Lambda.Templates
你应该看到成功安装的结果:

注意Lambda ASP.NET Core Web API的短名称:serverless.AspNetCoreWebAPI。
然后,运行下面的命令来创建项目。
dotnet new serverless.AspNetCoreWebAPI --name TwilioWebhookLambda.WebApi --output .
上面的命令将创建一个新的项目,文件结构如下:

注意,模板会创建一个名为src的文件夹,并将项目放在该文件夹中。你可以将代码移到你的根文件夹中,但文章的其余部分将使用默认路径。
你将利用AWS Lambda的一个新功能--Function URLs,使函数公开可用。为了让这个功能与你的API一起使用,你需要安装Amazon.Lambda.AspNetCoreServer.Hosting NuGet包。在终端窗口中,导航到项目文件夹并运行:
cd src/TwilioWebhookLambda.WebApi
dotnet add package Amazon.Lambda.AspNetCoreServer.Hosting
然后,在你的IDE中打开Startup.cs,更新ConfigureServices 方法,使其看起来像这样:
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddAWSLambdaHosting(LambdaEventSource.HttpApi);
}
Lambda Function URLs在幕后使用HttpApi,所以你需要使用LambdaEventSource.HttpApi 作为事件源类型。
你将需要Amazon Lambda Tools .NET工具来通过命令行部署该函数。你可以通过运行下面的命令来安装它:
dotnet tool install -g Amazon.Lambda.Tools
亚马逊Lambda工具,使用aws-lambda-tools-defaults.json文件来获取一些安装的细节。不幸的是,它并没有附带所有需要的值。例如,你可以在这个文件中存储运行时间和函数的名称,这样你就不必在每次从头部署时不断地输入它。
打开该文件并更新它,使它看起来像这样:
{
"profile": "",
"region": "",
"configuration": "Release",
"function-runtime": "dotnet6",
"function-memory-size": 256,
"function-timeout": 30,
"function-handler": "TwilioWebhookLambda.WebApi",
"function-name": "TwilioWebhookLambda-WebApi",
"function-url-enable": true
}
如果你不提供配置文件和区域值,它将使用你的AWS配置中的默认配置文件和区域。如果你想覆盖默认值,也要更新这些值。
然后,通过运行以下命令部署Lambda函数:
dotnet lambda deploy-function
你的Lambda函数需要一个IAM角色来执行。附在这个角色上的策略决定了该函数的权限。默认情况下,亚马逊Lambda工具会代表你创建一个角色,并将其附加到该函数上。
在部署过程中,它列出了现有的角色以及一个创建新角色的选项:

选择 "创建新的IAM角色"选项。
给它一个描述性的名字,比如TwilioWebhookLambda-WebApi-Role,这样当你在IAM仪表盘中看到它时,就可以很容易地确定它的目的。
下一步是选择IAM策略。你的项目将需要Amazon S3的访问权来存储通话记录。另外,能够访问CloudWatch的日志总是很有帮助。所以从列表中选择3 - AWSLambdaExecute:

作为一个最佳实践,你应该开发自定义策略,只授予最小的必要权限。
在部署完成后,你应该看到成功部署的消息:

上面显示的公开可用的URL只是因为你在aws-lambda-tools-defaults.json 文件中启用了Function URL特性而创建的:
"function-url-enable": true
如果没有这个功能,你将无法使用Lambda函数作为webhook处理程序。
现在在浏览器中打开该URL,你应该看到默认的GET/端点结果:

该API与其他API一样工作。这个模板带有一个名为ValuesController的控制器样本。将*/api/values*添加到你的函数URL中,以测试该控制器:

你应该看到浏览器上显示一个字符串数组(value1和value2)。
你刚刚将你的ASP.NET Core网络API部署到Lambda,并使其公开可用。干得好!
接收来电
Twilio .NET SDK和ASP.NET的辅助库使构建Twilio应用程序变得更加容易。在本教程中,你将使用SDK来生成TwiML,使用辅助库来响应webhook请求。通过NuGet添加SDK和辅助库:
dotnet add package Twilio
dotnet add package Twilio.AspNet.Core
在Controllers文件夹下,添加一个名为IncomingCallController.cs的新文件,并用以下代码替换其内容:
using Microsoft.AspNetCore.Mvc;
using Twilio.AspNet.Core;
using Twilio.TwiML;
namespace TwilioWebhookLambda.WebApi.Controllers;
[ApiController]
[Route("api/[controller]")]
public class IncomingCallController : TwilioController
{
[HttpPost]
public TwiMLResult Index()
{
var response = new VoiceResponse();
response.Say("Hello. Please leave a message after the beep.");
return TwiML(response);
}
}
在终端,部署更新后的函数:
dotnet lambda deploy-function
你应该在屏幕上得到一个成功的更新信息:

在这一点上,你有一个公开可用的端点,但Twilio还不知道它:
转到Twilio控制台。选择你的账户,然后点击左侧窗格中的电话号码→管理→活动号码。(如果电话号码不在左侧窗格中,请点击探索产品,然后点击电话号码)。

你不会永久拥有Twilio号码;相反,你租用它们,直到你释放它们。如果你在10天的宽限期后释放一个号码,它就会回到号码池中。
点击你想在项目中使用的电话号码,向下滚动到语音部分。
在 "A Call Comes In"标签下,将下拉菜单设置为Webhook,旁边的文本字段是你的Lambda Function URL,后缀 为 /IncomingCall路径,下一个下拉选项是HTTP POST,然后点击保存。它应该看起来像这样。

测试一下,拨打你的Twilio号码,你应该听到*"你好。请在哔声后留言"。* 它实际上并没有等待消息,但至少你知道你已经实现了一个传入语音网络钩子。当你的Twilio号码接到一个电话时,你的代码就会被执行。
在下一节,你将处理第二个webhook类型。呼叫状态更新。
接收呼叫状态更新
在Controllers文件夹下创建一个新文件,名为CallStatusChangeController.cs,代码如下:
using Microsoft.AspNetCore.Mvc;
using Twilio.AspNet.Core;
namespace TwilioWebhookLambda.WebApi.Controllers;
[ApiController]
[Route("api/[controller]")]
public class CallStatusChangeController : TwilioController
{
private readonly ILogger<CallStatusChangeController> _logger;
public CallStatusChangeController(ILogger<CallStatusChangeController> logger)
{
_logger = logger;
}
[HttpPost]
public async Task Index()
{
var form = await Request.ReadFormAsync();
var to = form["To"];
var callStatus = form["CallStatus"];
var fromCountry = form["FromCountry"];
var duration = form["Duration"];
_logger.LogInformation(
"Message to {to} changed to {callStatus}. (from country: {fromCountry}, duration: {duration})",
to, callStatus, fromCountry, duration);
}
}
这段代码记录了一些发布到你的webhook的值。
回到Twilio控制台中的活动号码配置,用你的Lambda函数URL后缀*/CallStatusChange*来更新**"呼叫状态变化"字段,如下图所示:

保存你的配置,然后使用dotnet lambda deploy-function ,再次部署你的项目。
现在再次拨打你的Twilio号码,呼叫完成后,你应该在CloudWatch中看到回调日志。

当呼叫状态变为 "完成"时,你会收到这个消息。
你也可以使用Twilio控制台来查看所有的呼叫记录。在左窗格上点击监控→呼叫。
在列表中找到呼叫,并点击呼叫SID链接,查看详情:

在Request Inspector部分,你可以看到所有的回调及其请求和响应的细节。

接下来,你将研究第三种也是最后一种类型的语音网络勾选。录音状态更新。
接收录音状态更新
在你录制任何东西之前,请确保阅读这篇文章。录制语音和视频通信的法律考虑:
创建一个名为RecordingStatusChangeController 的新控制器,并用下面的代码替换其内容:
using Microsoft.AspNetCore.Mvc;
using Twilio.AspNet.Core;
namespace TwilioWebhookLambda.WebApi.Controllers;
[ApiController]
[Route("api/[controller]")]
public class RecordingStatusChangeController : TwilioController
{
private readonly ILogger<RecordingStatusChangeController> _logger;
public RecordingStatusChangeController(ILogger<RecordingStatusChangeController> logger)
{
_logger = logger;
}
[HttpPost]
public async Task Index()
{
var form = await Request.ReadFormAsync();
var callSid = form["CallSid"];
var recordingStatus = form["RecordingStatus"];
var recordingUrl = form["RecordingUrl"];
_logger.LogInformation(
"Recording status changed to {recordingStatus} for call {callSid}. Recording is available at {recordingUrl}"
,recordingStatus, callSid, recordingUrl);
}
}
与状态变化处理程序类似,这段代码只记录一些请求细节。一旦你看到所有webhooks工作正常,你将用更有意义的代码来更新实现。
你还需要修改IncomingCallController ,并替换Index 方法中的代码,如下图:
var response = new VoiceResponse();
response.Say("Hello. Please leave a message after the beep.");
response.Record(
timeout: 10,
recordingStatusCallback: new Uri("/api/RecordingStatusChange", UriKind.Relative)
);
return TwiML(response);
现在你要告诉Twilio你想记录电话的内容。你也在指定接收录音状态更新的webhook URL。与其他webhook类型不同,Twilio控制台中没有设置录音状态回调的字段。
部署这个更新并再次拨打你的号码。这一次,你应该可以在提示音后留言。一旦你完成了这些,检查你的CloudWatch日志,你应该看到两个状态更新。一个是呼叫状态,一个是录音状态:

正如你在日志中看到的,录音的URL默认是公开的,但录音有很长的随机名称,所以它们不能被未经授权的人迭加下载。为了提高录音的安全性,你可以在账户中的语音设置中启用Enforce HTTP Auth on Media URLs和语音录音加密选项。
将录音的MP3文件保存到Amazon S3桶中
现在让我们看看如何从你的ASP.NET Core项目中检索录音文件并将其上传到Amazon S3桶中。
截至2022年5月,Twilio有一个内置功能,可以将录音存储在Amazon S3桶中。然而,在本文中,你将使用不同的方法,从你的Lambda函数中以编程方式上传MP3文件。
首先,你将需要一个S3桶来存储文件。要创建这个桶,请进入AWS管理控制台,搜索S3:

然后,点击链接,进入S3服务仪表板。
点击创建水桶按钮:

给它一个描述性的和全球唯一的名字,接受所有的默认值,然后点击屏幕底部的创建桶按钮。
你应该在水桶列表中看到你的水桶:

亚马逊S3桶的名字是全局性的。如果别人创建了一个名为my-twilio-call-recordings的桶,你也不能使用这个名字。你可以在AWS文档中找到更多桶的命名规则。
在你的应用程序中,你需要安装AWS SDK包来与Amazon S3的API对话。
在终端,运行以下命令:
dotnet add package AWSSDK.S3
更新RecordingStatusChangeController 代码,如下所示:
using Amazon.S3;
using Amazon.S3.Transfer;
using Microsoft.AspNetCore.Mvc;
using Twilio.AspNet.Core;
namespace TwilioWebhookLambda.WebApi.Controllers;
[ApiController]
[Route("api/[controller]")]
public class RecordingStatusChangeController : TwilioController
{
[HttpPost]
public async Task Index()
{
string recordingUrl = Request.Form["RecordingUrl"];
string fileName = $"{recordingUrl.Substring(recordingUrl.LastIndexOf("/") + 1)}.mp3";
string bucketName = "my-twilio-call-recordings";
using HttpClient client = new HttpClient(); // use HttpClient factory in production
using HttpResponseMessage response = await client.GetAsync(recordingUrl);
using Stream recordingFileStream = await response.Content.ReadAsStreamAsync();
using var s3Client = new AmazonS3Client();
using var transferUtility = new TransferUtility(s3Client);
await transferUtility.UploadAsync(recordingFileStream, bucketName, fileName);
}
}
确保用你的桶的名字来设置bucketName 。
在这段代码中,当你收到录音URL时,你提取录音文件名,通过流检索文件内容,并将流传给S3 TransferUtility,后者将其作为{fileName}.mp3上传到Amazon S3桶。
在设置过程中,你没有明确告诉AWS,你的Lambda函数应该有对S3桶的访问权。所以你可能会想,你怎么会有这样的权限。原因是你选择了AWSLambdaExecute策略来附加到你的函数的角色。因此,如果你到IAM仪表板上搜索AWSLambdaExecute,你应该看到该策略的权限是这样定义的。

你可以看到这个策略有将对象放入所有S3桶的权限。由于这是一个演示项目,我决定保持简单。然而,在生产中,我建议编写你自己的策略,并给予最低限度的权限,例如使用资源的名称而不是使用通配符。你可以在这里阅读更多关于这个问题的内容。IAM最佳实践。应用最小特权权限
部署API的最终版本并再次调用你的号码。
在你完成通话后不久,你应该在你的桶里看到录音。

由于本文的重点是使用AWS Lambda来响应Twilio webhooks,所以本文不涉及保护你的端点。要了解更多关于Webhooks安全的信息,你可以阅读这篇文章。Webhooks安全。
总结
祝贺你!你涵盖了三种类型的webhooks。你涵盖了Twilio语音服务的三种类型的Webhooks,并为所有这些服务实现了处理程序。此外,你还成功地将录音下载到你自己的存储器中。后来,你可以使用Amazon S3 Glacier下载或将文件移动到冷库。当你能以编程方式管理所有这些时,可能性是无穷的。例如,你可以使用亚马逊Transcribe将通话录音转录为文本,或者你可以在记录-verb上使用Twilio的transcribe属性。