使用AWS Lambda和.NET对Twilio Webhooks进行响应(详细教程)

121 阅读15分钟

在这篇文章中,你将学习如何用.NET 6开发一个web API来处理Twilio webhooks,并将其部署到AWS Lambda。你还将学习如何将通话录音作为MP3文件保存到AWS S3

什么是webhooks?

在今天这个API驱动的世界里,整合应用比以往任何时候都要容易。大多数时候,你可以从外部系统的API中获得你所需要的信息,但有时你想在事情发生时得到外部系统的通知。这就是webhooks的作用。你向外部系统注册你自己的端点,当你要找的事件发生时,他们会向你的端点发布数据。

Twilio Webhooks

你能从webhooks得到的数据类型取决于Twilio的服务。对于Twilio语音API,有几种类型的webhooks,在本教程中你将使用其中的三种:

  1. 来电语音呼叫
  2. 状态回拨
  3. 录音状态回拨

来电语音呼叫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,并勾选访问密钥 - 程序化访问复选框:

IAM user creation page with the "user name" field set to "twilio-webhook-user", and "AWS credential type"  set to "Access key - Programmatic access".

点击 "下一步",右下方的权限按钮。

然后,选择直接附加现有策略,并选择AdministratorAccess。

Set permissions page where the user selected the "Attach existing policies directly" tab, and selected the "AdministractorAccess" policy.

点击 "下一步",右下角的标签按钮。标签是可选的(也是相当有价值的信息),为你创建的资源添加描述性的标签是一个好的做法。由于这是一个演示项目,你可以跳过这一步,点击下一步。审查"按钮。

在审查页面上确认你的选择,它应该看起来像这样:

IAM user creation review page showing the previous selections. "User name" is "twilio-webhook-user", "AWS access type" is "Programmatic access - with an access key", the user is given "AdministratorAccess".

然后,单击 "创建用户"按钮。

在用户创建过程的最后一步,你应该第一次和最后一次看到你的凭证。

在你按下关闭按钮之前,记下你的访问密钥ID秘密访问密钥

现在,打开一个终端窗口,运行以下命令:

aws configure

你应该看到一个AWS访问密钥ID的提示。复制并粘贴你的访问密钥ID,然后按回车键。

然后,复制并粘贴你的秘密访问密钥,并按回车键:

Terminal window showing access key id and secret access key have been entered and default region name is prompted

当提示时,输入us-east-1作为默认区域名称 ,然后按回车键。

在这个例子中,我将使用us-east-1区域。区域是AWS拥有其数据中心的地理位置。在生产部署中,尽可能地靠近你的客户,以减少延迟,这是一个好的做法。由于这是一个演示项目,为了方便,你可以使用us-east-1,因为它是AWS管理控制台的默认区域。你可以在这个文件中找到更多关于AWS区域的信息。区域和可用区

作为默认的输出格式,输入json并按回车键。

为了确认你已经正确配置了你的AWS配置文件,请运行以下命令:

aws configure list

输出应该是这样的:

Terminal window showing the details of the AWS account configured in the previous steps. Profile: not set, access_key: partially masked, secret_key: partially masked, region: us-east-1.

现在你已经设置了你的AWS凭证,你可以继续设置代码了。

为AWS Lambda创建一个ASP.NET Core项目

你可以从GitHub下载完成的项目。不过,本文将提供分步说明,以便自己进行设置。

打开一个终端,导航到将成为你的项目根的目录。

你将在示例项目中使用Lambda ASP.NET Core Web API项目模板。因此,首先,通过运行以下命令来安装Lambda模板:

dotnet new -i Amazon.Lambda.Templates

你应该看到成功安装的结果:

Terminal window showing Amazon.Lambda.Templates has been installed successfully

注意Lambda ASP.NET Core Web API的短名称:serverless.AspNetCoreWebAPI

然后,运行下面的命令来创建项目。

dotnet new serverless.AspNetCoreWebAPI --name TwilioWebhookLambda.WebApi --output .

上面的命令将创建一个新的项目,文件结构如下:

File structure show an "src" folder, with a "TwilioWebhookSample.WebApi" subfolder which has the .NET project files in it.

注意,模板会创建一个名为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工具会代表你创建一个角色,并将其附加到该函数上。

在部署过程中,它列出了现有的角色以及一个创建新角色的选项:

Terminal window asking the name of the IAM role

选择 "创建新的IAM角色"选项。

给它一个描述性的名字,比如TwilioWebhookLambda-WebApi-Role,这样当你在IAM仪表盘中看到它时,就可以很容易地确定它的目的。

下一步是选择IAM策略。你的项目将需要Amazon S3的访问权来存储通话记录。另外,能够访问CloudWatch的日志总是很有帮助。所以从列表中选择3 - AWSLambdaExecute

Terminal window showing a list of IAM policies to select from including "3) AWSLambdaExecute (Provides Put, Get access to S3 and full access to CloudWatch logs.)

作为一个最佳实践,你应该开发自定义策略,只授予最小的必要权限。

在部署完成后,你应该看到成功部署的消息:

Terminal window showing the output of successful Lambda function creation. It"s showing the public function URL

上面显示的公开可用的URL只是因为你在aws-lambda-tools-defaults.json 文件中启用了Function URL特性而创建的:

"function-url-enable": true

如果没有这个功能,你将无法使用Lambda函数作为webhook处理程序。

现在在浏览器中打开该URL,你应该看到默认的GET/端点结果:

Browser showing the welcome message of the deployed API: Welcome to running ASP.NET Core on AWS Lambda.

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

Browser showing path /api/values endpoint called with no parameters and showing the results value1 and value2

你应该看到浏览器上显示一个字符串数组(value1和value2)。

你刚刚将你的ASP.NET Core网络API部署到Lambda,并使其公开可用。干得好!

接收来电

Twilio .NET SDKASP.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

你应该在屏幕上得到一个成功的更新信息:

Terminal window showing that the existing Lambda function has been updated successfully.

在这一点上,你有一个公开可用的端点,但Twilio还不知道它:

转到Twilio控制台。选择你的账户,然后点击左侧窗格中的电话号码管理活动号码。(如果电话号码不在左侧窗格中,请点击探索产品,然后点击电话号码)。

Twilio console showing Active Numbers in the account listed

你不会永久拥有Twilio号码;相反,你租用它们,直到你释放它们。如果你在10天的宽限期后释放一个号码,它就会回到号码池中。

点击你想在项目中使用的电话号码,向下滚动到语音部分。

在 "A Call Comes In"标签下,将下拉菜单设置为Webhook,旁边的文本字段是你的Lambda Function URL,后缀/IncomingCall路径,下一个下拉选项是HTTP POST,然后点击保存。它应该看起来像这样。

Voice section on the Twilio Phone Number configuration page. Under the "a call comes in" label, the first dropdown is set to Webhook, the text field next to it is set to a AWS Lambda URL with path /IncomingCall, and the dropdown next to it is set to 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*来更新**"呼叫状态变化"字段,如下图所示:

Text field with label "CALL STATUS CHANGES", set to the AWS Lambda Function URL suffixed with the /api/CallStatusChange path.

保存你的配置,然后使用dotnet lambda deploy-function ,再次部署你的项目。

现在再次拨打你的Twilio号码,呼叫完成后,你应该在CloudWatch中看到回调日志。

CloudWatch logs showing the logged information form the status callback message

当呼叫状态变为 "完成"时,你会收到这个消息。

你也可以使用Twilio控制台来查看所有的呼叫记录。在左窗格上点击监控呼叫

在列表中找到呼叫,并点击呼叫SID链接,查看详情:

Twilio call logs showing the latest Call SID and timestamp

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

Twilio console showing the request and response details in the 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日志,你应该看到两个状态更新。一个是呼叫状态,一个是录音状态:

CloudWatch logs showing callback logs for call status change and call recording status

正如你在日志中看到的,录音的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:

AWS Management Console showing search results for S3

然后,点击链接,进入S3服务仪表板。

点击创建水桶按钮:

S3 dashboard showing Create a bucket section with brief info about S3 and a Create bucket button

给它一个描述性的和全球唯一的名字,接受所有的默认值,然后点击屏幕底部的创建桶按钮。

你应该在水桶列表中看到你的水桶:

Amazon S3 dashboard showing the newly created bucket in the list

亚马逊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,你应该看到该策略的权限是这样定义的。

Permissions of AWSLambdaExecute policy are displayed showing the policy has PutObject access to the entire S3 service

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

部署API的最终版本并再次调用你的号码。

在你完成通话后不久,你应该在你的桶里看到录音。

Amazon S3 dashboard showing the objects in the bucket. The newly saved MP3 file or the recording is shown

由于本文的重点是使用AWS Lambda来响应Twilio webhooks,所以本文不涉及保护你的端点。要了解更多关于Webhooks安全的信息,你可以阅读这篇文章。Webhooks安全

总结

祝贺你!你涵盖了三种类型的webhooks。你涵盖了Twilio语音服务的三种类型的Webhooks,并为所有这些服务实现了处理程序。此外,你还成功地将录音下载到你自己的存储器中。后来,你可以使用Amazon S3 Glacier下载或将文件移动到冷库。当你能以编程方式管理所有这些时,可能性是无穷的。例如,你可以使用亚马逊Transcribe将通话录音转录为文本,或者你可以在记录-verb上使用Twilio的transcribe属性