保持合规性是你的信息传递战略的一个重要部分。它确保你的发件人得到运营商的高度信任,并因此经历较低的过滤和较低的被阻止风险。
保持一个干净的退出名单是合规信息传递的关键支柱之一。Twilio平台通过自动退出管理帮助你维护客户的信任(更多信息在这里),这是一个很好的开始,但在某些情况下,退出信息可能会丢失。此外,Twilio还没有提供一个API来检索退出数据。
所有这些都说明:我们Twilio强烈建议你自己维护和存储退出数据。在这篇博文中,我将向你展示如何在Twilio平台上通过无服务器函数和同步来存储退出信息。
教程先决条件
在我们开始构建之前,你需要确保你有一个Twilio的账户。你可以在这里注册一个免费账户。
一旦你有了账户,请注意以下细节--你以后会需要它们。
有了这些,你就可以开始了。
正如我提到的,我们将利用Twilio功能和同步。但在我们开始写代码之前,这里是我们的高级计划。
- 创建一个消息服务并添加一个电话号码
- 创建一个Sync服务,并在其中创建一个Map对象;Map将是我们的opt-out数据存储。
- 创建一个函数,一旦客户向电话号码发送消息,该函数将被调用。该函数将负责。
- 分析消息的主体,看它是否是opt-out或opt-in的字样
- 如果邮件正文是 "退出 "字样,该函数将把发件人号码添加到同步地图中。
- 如果正文是一个选择加入的词,该函数将从同步地图中删除发件人号码。
- 更新Messaging Service的设置,将传入信息的webhook指向该函数的URL。
下面是我们要建立的解决方案的图示。

同步设置
如前所述,我们将在同步地图中存储我们的选择退出数据。
首先,我们需要一个同步服务,然后在该服务中建立一个地图。在你的Twilio账户中,一个名为 "默认服务 "的同步服务应该已经存在--使用这个服务是可以的,在这篇博文中,我将使用默认服务。或者,你可以在Twilio控制台的同步服务页面创建一个新的服务。
为了以后配置环境变量,你将需要同步服务的SID。如果你进入Twilio控制台的同步服务页面,你可以看到所有服务的SID(注意你将使用的服务的SID)。
接下来,导航到所选的同步服务,在左边选择地图,然后创建一个新的同步地图。你必须为你的新地图提供一个名称--确保把时间留到空白。地图创建后,记下它的SID......我们以后会需要它。
一旦你有了一个同步服务和地图,我们就可以继续了。
开发者环境设置
在我们继续之前,让我们确保你有你需要的软件。
我将在本教程中使用Typescript,但用JavaScript也应该可以。
现在我们可以开始编码了
创建项目
我们将首先使用Twilio Serverless Toolkit创建一个项目。对于这个运行,在你的shell中发出以下命令。
twilio serverless:init opt-out-data-storage --typescript
这里有几个注意事项。
- 在命令中,我使用
opt-out-data-storage作为我的项目名称,请随意使用不同的名称 - 添加
--typescript参数将创建一个准备用于Typescript的项目。如果你喜欢JavaScript,你可以省略这个参数。
完成这一步后,你就有了一个可以在本地环境中运行或直接部署到Twilio Functions的项目。
你会发现在src/functions 文件夹下有几个函数的例子,在src/assets 下有几个资产的例子,你可以安全地删除它们。(或者你可以忽略它们,以你喜欢的方式为准)。
配置环境
为了运行代码,我们需要将环境变量设置到位。进入项目根目录下的.env ,并更新文件,使其具有以下键和值。确保将占位符替换为你在前面步骤中收集的值。
ACCOUNT_SID=<Twilio Account SID>
AUTH_TOKEN=<Twilio Auth Token>
SYNC_SERVICE_SID=<Sync Service SID>
SYNC_MAP_SID=<Sync Map SID>
创建功能
现在是时候创建你将在其中工作的文件了。导航到src/functions,并创建一个名为capture-opt-out.protected.ts的文件。
虽然你可以自由地为你的函数选择任何其他名称,但要确保名称以.protected.ts (或.protected.js )结尾,因为这定义了函数的可见性级别。
我们将从下面的片段开始,因为它包含了完成一个函数的最基本代码。
// Imports global types
import '@twilio-labs/serverless-runtime-types'
// Fetches specific types
import {
Context,
ServerlessCallback,
ServerlessFunctionSignature,
} from '@twilio-labs/serverless-runtime-types/types'
export const handler: ServerlessFunctionSignature = async function (
context: Context,
event: {},
callback: ServerlessCallback,
) {
callback(null, 'Hello from Twilio Functions!')
}
我们知道我们的函数将接受哪些参数,以及它将使用哪些环境变量。反映在代码中,这将看起来像。
// Imports global types
import '@twilio-labs/serverless-runtime-types'
// Fetches specific types
import {
Context,
ServerlessCallback,
ServerlessFunctionSignature,
} from '@twilio-labs/serverless-runtime-types/types'
type InboundMessage = {
Body: string,
From: string
}
type OptOutContext = {
SYNC_SERVICE_SID: string,
SYNC_MAP_SID: string
}
export const handler: ServerlessFunctionSignature<OptOutContext, InboundMessage> = function (
context: Context<OptOutContext>,
event: InboundMessage,
callback: ServerlessCallback,
) {
callback(null, 'Hello from Twilio Functions!')
}
接下来,我们可以添加一个选择退出和选择加入的关键词列表(这里我将使用默认列表。你可以在你的信息服务中的 "退出管理 "部分找到默认值)。
如果需要,你可以利用Twilio的高级退出功能来修改列表,这篇博文详细描述了如何做到这一点。
// Imports global types
import '@twilio-labs/serverless-runtime-types'
// Fetches specific types
import {
Context,
ServerlessCallback,
ServerlessFunctionSignature,
} from '@twilio-labs/serverless-runtime-types/types'
type InboundMessage = {
Body: string,
From: string
}
type OptOutContext = {
SYNC_SERVICE_SID: string,
SYNC_MAP_SID: string
}
const optOutWords = new Set<string>([
'stop',
'cancel',
'end',
'quit',
'unsubscribe',
'stopall',
])
const optInWords = new Set<string>([
'start',
'yes',
'unstop',
])
export const handler: ServerlessFunctionSignature<OptOutContext, InboundMessage> = function (
context: Context<OptOutContext>,
event: InboundMessage,
callback: ServerlessCallback,
) {
callback(null, 'Hello from Twilio Functions!')
}
现在我们可以开始工作,一旦我们检测到选择退出或选择加入的关键词,就可以从地图上添加或删除一个号码的逻辑。
首先,我们将构建一个信息传递响应对象。我们将用它来发送反馈,我们将总是发送一个空的响应,这意味着没有消息将被发送到发送者那里。其次,我们将有if-else的三个分支:opt-out、opt-in和default。默认分支将不做任何事情;我们希望忽略所有不是opt-out或opt-in关键字的东西。
// Imports global types
import '@twilio-labs/serverless-runtime-types'
// Fetches specific types
import {
Context,
ServerlessCallback,
ServerlessFunctionSignature,
} from '@twilio-labs/serverless-runtime-types/types'
type InboundMessage = {
Body: string,
From: string
}
type OptOutContext = {
SYNC_SERVICE_SID: string,
SYNC_MAP_SID: string
}
const optOutWords = new Set<string>([
'stop',
'cancel',
'end',
'quit',
'unsubscribe',
'stopall',
])
const optInWords = new Set<string>([
'start',
'yes',
'unstop',
])
export const handler: ServerlessFunctionSignature<OptOutContext, InboundMessage> = function (
context: Context<OptOutContext>,
event: InboundMessage,
callback: ServerlessCallback,
) {
// Response object
const response = new Twilio.twiml.MessagingResponse()
if (optOutWords.has(event.Body.trim().toLowerCase())) {
console.log('We\'ve got opt-out', event)
// Opt-out path -> here we should add number to the map
callback(null, response)
} else if (optInWords.has(event.Body.trim().toLowerCase())) {
console.log('We\'ve got opt-in', event)
// Opt-in path -> here we should remove number from the map
callback(null, response)
} else {
callback(null, response)
}
}
接下来,我们将编写代码,如果我们检测到一个opt-out关键字,就在地图中添加一个数字。这里有几个注意事项。
- 如果地图中已经包含了数字,就会出现一个异常。如果一个选择退出的用户已经再次发送了一个选择退出的关键字,这就会发生。我们可以忽略这个异常,因为我们存储中的状态不应该改变。
- 我们将使用发件人号码作为我们新的地图项目的 "键",并将该 "数据 "保留为一个空的JSON对象。在生产中,你可以向数据对象添加其他有用的信息,例如时间戳。
// Imports global types
import '@twilio-labs/serverless-runtime-types'
// Fetches specific types
import {
Context,
ServerlessCallback,
ServerlessFunctionSignature,
} from '@twilio-labs/serverless-runtime-types/types'
type InboundMessage = {
Body: string,
From: string
}
type OptOutContext = {
SYNC_SERVICE_SID: string,
SYNC_MAP_SID: string
}
const optOutWords = new Set<string>([
'stop',
'cancel',
'end',
'quit',
'unsubscribe',
'stopall',
])
const optInWords = new Set<string>([
'start',
'yes',
'unstop',
])
export const handler: ServerlessFunctionSignature<OptOutContext, InboundMessage> = function (
context: Context<OptOutContext>,
event: InboundMessage,
callback: ServerlessCallback,
) {
// Response object
const response = new Twilio.twiml.MessagingResponse()
if (optOutWords.has(event.Body.trim().toLowerCase())) {
console.log('We\'ve got opt-out', event)
context.getTwilioClient()
.sync
.services(context.SYNC_SERVICE_SID)
.syncMaps(context.SYNC_MAP_SID)
.syncMapItems
.create({
key: event.From,
data: {},
})
.then(_ => {
console.log('Successfully stored map item')
callback(null, response)
})
.catch(err => {
console.log(err)
callback(null, response)
},
)
} else if (optInWords.has(event.Body.trim().toLowerCase())) {
console.log('We\'ve got opt-in', event)
// Opt-in path -> here we should remove number from the map
callback(null, response)
} else {
callback(null, response)
}
}
现在,让我们来看看最后一段代码--选择加入分支。在这里,我们从地图中删除一个发件人号码。再一次,如果该号码不在Map中,我们将忽略这个异常。
// Imports global types
import '@twilio-labs/serverless-runtime-types'
// Fetches specific types
import {
Context,
ServerlessCallback,
ServerlessFunctionSignature,
} from '@twilio-labs/serverless-runtime-types/types'
type InboundMessage = {
Body: string,
From: string
}
type OptOutContext = {
SYNC_SERVICE_SID: string,
SYNC_MAP_SID: string
}
const optOutWords = new Set<string>([
'stop',
'cancel',
'end',
'quit',
'unsubscribe',
'stopall',
])
const optInWords = new Set<string>([
'start',
'yes',
'unstop',
])
export const handler: ServerlessFunctionSignature<OptOutContext, InboundMessage> = function (
context: Context<OptOutContext>,
event: InboundMessage,
callback: ServerlessCallback,
) {
// Response object
const response = new Twilio.twiml.MessagingResponse()
if (optOutWords.has(event.Body.trim().toLowerCase())) {
console.log('We\'ve got opt-out', event)
context.getTwilioClient()
.sync
.services(context.SYNC_SERVICE_SID)
.syncMaps(context.SYNC_MAP_SID)
.syncMapItems
.create({
key: event.From,
data: {},
})
.then(_ => {
console.log('Successfully stored map item')
callback(null, response)
})
.catch(err => {
console.log(err)
callback(null, response)
},
)
} else if (optInWords.has(event.Body.trim().toLowerCase())) {
console.log('We\'ve got opt-in', event)
context.getTwilioClient()
.sync
.services(context.SYNC_SERVICE_SID)
.syncMaps(context.SYNC_MAP_SID)
.syncMapItems(event.From)
.remove()
.then(_ => {
console.log('Successfully removed map item')
callback(null, response)
})
.catch(err => {
console.log(err)
callback(null, response)
},
)
} else {
callback(null, response)
}
}
这样,我们的函数就完成了
部署功能
下一步是将我们的代码部署到Twilio函数。要做到这一点,在你的项目根目录下执行以下命令。
npm run deploy
如果你在部署代码时遇到问题,请检查ACCOUNT_SID 和AUTH_TOKEN 环境变量是否在你的.env 文件中正确配置了。
一旦部署完成,你将看到你新创建的函数的URL。它看起来会像下面这样。
Functions:
[protected] https://capture-opt-out-0000-dev.twil.io/capture-opt-out
复制这个URL--我们已经准备好配置消息传递服务了。
配置一个消息传递服务
创建一个消息服务并为其添加一个电话号码不在本教程的范围之内。它可以通过Twilio控制台或通过API实现。
假设你已经有了一个消息服务,去你的Twilio控制台,导航到消息服务下的 "集成"部分。在 "集成"页面,找到 "传入信息"部分,选择 "发送网络钩子"单选按钮。将你在上一步中复制的功能URL输入到请求URL字段中,然后点击保存配置。
测试
为了测试我们的解决方案,向你的信息服务发送者库中的一个号码发送短信,短信内容为 "停止"(假设 "停止 "是选择退出的关键词之一)。你应该收到一个关于成功退出的自动回复。

一旦你收到自动回复,请检查发件人号码是否被添加到选择退出地图中。
你可以在Twilio控制台或使用Twilio CLI查看地图内容。CLI的命令是:。
twilio api:sync:v1:services:maps:items:list --service-sid ISXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX --map-sid=MPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX --properties='key'
输出应该是这样的。
Key
+1234567890
如果你在地图中看到发件人号码,说明我们的设置符合预期--现在我们可以测试选入流程了。为此,向你的短信服务的发件人池中的一个号码发送一条短信,内容为 "START"(假设START是你的选择加入关键词之一)。
一旦你收到自动回复说你已经成功选择加入,检查发件人号码是否已经从选择退出地图中删除。
总结
使用Twilio功能和同步,我们建立了可靠的选择退出列表,并将其存储在Twilio平台上,而不需要依赖任何第三方工具。这些数据可以在任何时候使用Twilio控制台、Twilio CLI或REST API查看和导出。我们的解决方案最棒的地方在于,它并不局限于SMS;你可以为其他信息传递渠道,如WhatsApp或你采用的任何其他渠道存储退出信息。
该项目的完整代码可在GitHub上找到:github.com/kuschanton/…
喜欢你建立的东西吗?请看我们博客上的其他一些函数和无服务器教程。