Amazon Pinpoint 通常用来通过 SMS 以及邮件的方式来给用户发通知,我们可以创建 Campaign,来动态的给圈选的用户在指定的时间、或某个事件发生的情况下,给用户发送指定模板的消息。更复杂的,我们还可以创建用户旅程的流程,来给用户发消息。
除了 SMS 和邮件,Pinpoint 还提供了 Push-notification,in-app messaging 的消息,我们甚至还可以通过 Lambda 还创建自定义的消息渠道。本文我们就来看一下如何使用 Lambda 创建自定义的渠道(custom channel)并使用它来发送消息给用户。
本文中,我们希望给用户的 Whatsapp 账号发送消息,实现原理实际上就是在 Lambda 函数内通过调用 Whatsapp 的 API 来发消息。这就需要几个前提:
Whatsapp 是 Facebook 旗下的产品,它提供 Business Platform API,可以用来直接发送 WhatsApp 消息,而无需通过类似 Twilio 这样的服务。开通和配置的过程不是本文的内容,这里就不做说明。我们需要能够通过 API 调用来发消息。配置完成后,可以同下面的页面来测试:
它通过临时的 Token 使用 API 发消息;这里的 Phone number ID 是我们的注册的 Business 账号下使用的电话的 Id;To 就是我们要发送消息的对象。
发送消息后,我们就可以在网页版的 WhatsApp 应用,或手机应用上看到收到的测试消息。
验证消息接收无误以后,就可以使用 API 调用发送消息了。
从上面的测试接口页面,我们可以知道测试接口需要几个参数:
根据 WhatsApp 的机制,我们需要先创建一个消息模板,然后使用该模板发送消息。所以我们需要创建一个 template:
该消息的内容为: Hi {{1}}, the {{2}} in your order is not paid yet. Go to pay the order and get extra points!。使用两个变量用来替换消息的内容,实现定制。右侧是消息的展示效果。
接下来,我们创建一个 lambda 函数,可以使用 AWS 控制台创建一个空的、NodeJs16 版本 Lambda,index.js 的内容如下:
exports.handler = async (event) => {
console.log('Event', event)
if (event.Endpoints === undefined) {
const errorText = 'Unsupported event type. event. Endpoints not defined.'
console.error(errorText)
throw new Error(errorText)
}
const endpoints = event.Endpoints
for (var endpointId in endpoints) {
const endpoint = endpoints[endpointId]
const phoneId = endpoint.Address
const customMessage = JSON.parse(event.Data)
const templateName = customMessage.templateName
const templateParams = customMessage.params
await sendWhatsAppTemplateMessage(phoneId, templateName, templateParams)
}
}
这只是一个处理函数,具体发消息的方法下面再说。Pinpoint 使用定制渠道发送消息的时候,消息的格式如下:
{
"Message":{},
"Data":"The payload that's provided in the CustomMessage object in MessageConfiguration",
"ApplicationId":"3a9b1f4e6c764ba7b031e7183example",
"CampaignId":"13978104ce5d6017c72552257example",
"TreatmentId":"0",
"ActivityId":"575cb1929d5ba43e87e2478eeexample",
"ScheduledTime":"2022-10-08T19:00:16.843Z",
"Endpoints":{
"1dbcd396df28ac6cf8c1c2b7fexample":{
"ChannelType":"EMAIL",
"Address":"mary.major@example.com",
"EndpointStatus":"ACTIVE",
"OptOut":"NONE",
"Location":{
"City":"Seattle",
"Country":"USA"
},
"Demographic":{
"Make":"OnePlus",
"Platform":"android"
},
"EffectiveDate":"2022-10-01T01:05:17.267Z",
"Attributes":{
"CohortId":[
"42"
]
},
"CreationDate":"2022-10-01T01:05:17.267Z"
}
}
}
每个事件参数中会有一个 Endpoints 的对象,它保存要发送消息的目标端点,其中 key 就是 EndpointId,值就是这个端点的详情,在这个消息内容中,它使用 Email 发消息,目标地址是 mary.major@example.com。在本实例中,我们要发送 WhatsApp 消息,所以我们会给用户创建一个 WhatsApp 类型的 Endpoint,而它的 address 就是 WhatsApp 的手机号。
在使用 Pinpoint 发消息时,定制消息的内容放在 event.Data 中,而在这个数据中,我们将模板名和数据都放在这里,这样我们就可以使用同一个 Lambda 的函数,发送多种模板类型的消息。
const customMessage = JSON.parse(event.Data)
const templateName = customMessage.templateName
const templateParams = customMessage.params
还有,我们要掉用 Facebook 的 API,还需要使用 token,而这个 token 一般都需要加密保存,所以我们使用 AWS 的 Secret Manager 服务,创建一个加密的对象来存储,然后在 Lambda 函数中获取这个值,这样就能安全的使用这个token。
所以,我们打开 Secret Manager Console 界面,Store a new secret,选择 Other type of secret,以 FB_APP_TOKEN 为 key(在代码中要使用这个key来获取token),值就是 Facebook 里面的 token。
保存之后,我们需要在 Lambda 中得到这个 token 的值,通常的做法是在 Lambda 中创建一个环境变量,将刚才在 Secret Manager 里创建的安全对象的 ARN 值设置到这个环境变量的值。
所以我们打开 Lambda 的配置界面,按如下方式配置环境变量。
由于我们在 lambda 中使用这个环境变量的 key 来获取他的值,所以需要使用 FB_BUSINESS_PHONE_ID 和 FB_SECRET_ARN 作为key。
下面,就是js中所有的代码,包括处理人口以及发送 WhatsApp 消息的代码:
onst https = require('https')
const AWS = require('aws-sdk')
const BUSINESS_PHONE_ID = process.env.FB_BUSINESS_PHONE_ID
const PATH_WHATSAPP = '/v14.0/' + BUSINESS_PHONE_ID + '/messages'
let appToken
const secretManager = new AWS.SecretsManager()
const sendWhatsAppTemplateMessage = async (phoneId, templateName, templateParams) => {
if (appToken === undefined) {
await getFacebookSecrets()
}
const body = {
messaging_product: 'whatsapp',
recipient_type: 'individual',
to: phoneId,
type: 'template',
template: {
name: templateName,
language: { code: "en_US" },
components: templateParams
}
}
console.log('Send FB WhatsApp body', body)
const headers = {
Authorization: 'Bearer ' + appToken,
'Content-Type': 'application/json'
}
const options = {
host: 'graph.facebook.com',
path: PATH_WHATSAPP,
method: 'POST',
headers
}
const result = await new Promise((resolve, reject) => {
const req = https.request(options, (res) => {
let responseBody = ''
res.on('data', (chunk) => {
responseBody += chunk
})
res.on('end', () => {
resolve(responseBody)
})
})
req.on('error', (err) => {
console.error('Error sending FB WhatsApp message', err)
reject(err)
})
req.write(JSON.stringify(body))
req.end()
})
const resultObj = JSON.parse(result)
console.log('Send FB WhatsApp Message result', result)
if (resultObj.error !== undefined) {
console.error('Error sending FB WhatsApp message', resultObj)
return false
}
return true
}
const getFacebookSecrets = async () => {
if (process.env.FB_SECRET_ARN) {
const params = {
SecretId: process.env.FB_SECRET_ARN
}
const response = await secretManager.getSecretValue(params).promise()
const sec = JSON.parse(response.SecretString)
appToken = sec.FB_APP_TOKEN
} else {
appToken = null
}
}
exports.handler = async (event) => {
console.log('Event', event)
if (event.Endpoints === undefined) {
const errorText = 'Unsupported event type. event. Endpoints not defined.'
console.error(errorText)
throw new Error(errorText)
}
const endpoints = event.Endpoints
for (var endpointId in endpoints) {
const endpoint = endpoints[endpointId]
const phoneId = endpoint.Address
const customMessage = JSON.parse(event.Data)
const templateName = customMessage.templateName
const templateParams = customMessage.params
await sendWhatsAppTemplateMessage(phoneId, templateName, templateParams)
}
}
为了测试,我们还需要在 Pinpoint 中创建一个项目,并记录下项目的 Id 备用。然后还需要一个Endpoint,由于我们需要一个 WhatsApp 类型的,这是一个自定义的 Channel 类型,系统中肯定不会有现成的 Endpoint,所以我们使用 aws cli 工具创建一个。
首先创建一个 test-endpoint.json 文件,内容如下:
{
"ChannelType": "CUSTOM",
"Address": "8613800138000",
"Attributes": {
"Interests": [
"Technology",
"Music",
"Travel"
]
},
"Metrics": {
"technology_interest_level": 9.0,
"music_interest_level": 6.0,
"travel_interest_level": 4.0
}
}
其中,Address 就是你要测试使用的手机号,需要在前面加上国家码(即86)。
然后使用 AWS Cli 工具运行:
aws pinpoint update-endpoint --application-id f190affea10b4ce2b4a2cbbd85976c96 --profile MTProj --endpoint-
id test123 --endpoint-request file://test-endpoint.json
其中,–application-id 后面是之前创建的pinpoint项目的id,–profile 是我自己使用的认证方式。执行后,成功的话,就会返回新建的 Endpoint id:
{
"MessageBody": {
"Message": "Accepted",
"RequestID": "b6e9a334-0d51-4b5e-9b87-4aecaad7c966"
}
}
也可以通过命令获得endpoint信息:
aws pinpoint get-endpoint --application-id f190affea10b4ce2b4a2cbbd85976c96 --endpoint-id test123 --profile MTProj
最后,我们还需要合适的权限才能使用这个定制的渠道。
首先,我们需要知道它的调用逻辑,才能知道谁需要什么权限,来访问谁。在发送定制渠道的消息时,我们通过 API 创建一个 campaign 活动,在 campaign 里面创建一个动态的 Segment;它在执行的时候,会掉用 lambda 函数,而这个 Lambda 函数在执行的时候,又需要从 Secret Manager 读取 Token,所以它的调用关系如下:
pinpoint应用 -> lambda 函数 -> Secret Manager 对象。我们通过给这个 Lambda 函数设置一个 Resource Policy,来允许 Pinpoint 应用可以调用它,然后再给这个 Lambda 的role设置权限,让它能够访问 Secret Manager。
所以,先打开刚才创建的 Lambda 函数配置页面的 Permission 页:
在 Resource based policy的部分,添加一条permission:
选择 AWS service,选择 Other,然后手动输入,其中 Principal 输入 pinpoint.amazonaws.com,Source ARN 填你创建的 Pinpoint 项目的 ARN,我这里最后用星号(*),表示我所有的Pinpoint项目都能调用这个 Lambda 。然后选择Action 为 lambda:InvokeFunction。
然后,找到这个 Lambda 函数使用的角色,为这个角色添加Policy,来让它能够访问 Secret Manager。在角色的Permission页面,添加一个 Inline Policy,策略内容如下:
{
"Version": "2012-10-17",
"Statement": [
{
"Action": "secretsmanager:GetSecretValue",
"Resource": "arn:aws:secretsmanager:us-east-1:56xxxxxx027:secret:connect/facebook-S5G08O",
"Effect": "Allow"
}
]
}
Resource 的值就是我们之前在 Secret Manager 里创建的对象的 arn。
最后,我们在 Pinpoint 的console 页面,进入之前的项目,创建一个 Campaign 来测试这个新渠道消息。创建的时候,创建 Segment 的时候,我们需要创建一个 Segment,条件设置 Channel Type 为 custom,下面会出来预估的当前这个 Segment 条件下大概会有多少个Endpoint 会被触达。我们这里的测试环境只有一个 CUSTOM 类型的 Endpoint,所以只有1.
下一步,我们需要选择之前创建的 Lambda 函数,以及使用过 custom 类型的Endpoints。
然后下一步,选择立即开始,并最后保存这个 Campaign。
创建完成后,就会跳转到详情页,这个页面中,会展示这个活动圈选了几个 Endpoint,并触达了几个 Endpoint(这里的触达只是表示掉用了 Lambda 函数,至于 Lambda 函数是否成功执行,这里无法体现)。
注意:我们发送 Whatsapp 消息,使用定制的消息模板,并且模板中还使用变量,而这些变量,也需要通过 Campaign 传递到 Lambda 函数,然后在 API 中使用。 但是,我们使用 Console 进行测试的时候,并没有传递任何参数,所以,实际上,我们通过 Console 测试的时候,Lambda 函数会执行失败,因为没有 event.Data 数据。所以,我们需要通过 API 调用的方式,才能测试带自定义内容的消息。
最后,我们来看一下使用过 JS sdk进行API调用的实例:
const ppClient = new PinpointClient({
region: "us-east-1",
credentials: fromCognitoIdentityPool({
identityPoolId: <the_identityPoolId>,
client: new CognitoIdentityClient({ region: 'us-east-1' })
})
});
const dataObj = [
{
"type": "body",
"parameters": [
{"type": "text", "text": "Tuohumai"},
{"type": "text", "text": " Champion T Shirt"}
]
},
{
"type": "button",
"sub_type": "quick_reply",
"index": "0",
"parameters": [
{ "type": "payload", "payload": "button 0 clicked" }
]
}
]
const customMessage = { templateName: 'ordertimeoutnotification', params: dataObj }
const command = new CreateCampaignCommand({
ApplicationId: 'f190affea10b4ce2b4a2cbbd85976c96',
WriteCampaignRequest: {
Name: 'test-campaign-wa-message-with-api',
Description: 'campaign Desc',
SegmentId: 'b5b50ccb81644a3390751ee1e4fe27ac',
Schedule: {
StartTime: "IMMEDIATE"
},
MessageConfiguration: {
CustomMessage: {
Data: JSON.stringify(customMessage),
}
},
CustomDeliveryConfiguration: {
DeliveryUri: 'arn:aws:lambda:us-east-1:568xxxxxx9027:function:mt_pinpoint_whatsapp_custom_channel',
EndpointTypes: ["CUSTOM"]
}
}
});
const response = await ppClient.send(command);
console.log("Success", response);
在这个代码中,我们先创建了自定义消息所需的参数 dataObj,它跟我们在 WhatsApp 中创建的消息有关,它包括消息内容中要替换的变量 parameters,也有 Whatsapp 里面展示时添加的按钮,以及这个按钮的动作,这都是 WhatsApp 消息所需的内容,跟接口无关。然后我们将这个对象转成 String 以后,放到 MessageConfiguration.CustomMessage.Data 中。
WriteCampaignRequest 是使用接口创建 Campaign 的对象。通过这个接口,就可以创建一个立即运行的 Campaign,这样就能在 WhatsApp 应用中看到通知的消息了。