in lib/chat-message-streaming-examples-stack.ts [15:377]
constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// The code that defines your stack goes here
// Need deployment mechanism to check if they are deploying SMS or FB or Both demos and validate on that
// Get environment variables from context
const amazonConnectArn = this.node.tryGetContext("amazonConnectArn");
const contactFlowId = this.node.tryGetContext("contactFlowId");
const pinpointAppId = this.node.tryGetContext("pinpointAppId");
const smsNumber = this.node.tryGetContext("smsNumber");
const fbSecretArn = this.node.tryGetContext("fbSecretArn");
let enableFB = false;
let enableSMS = false;
// Validating that environment variables are present
if(amazonConnectArn === undefined){
throw new Error("Missing amazonConnectArn in the context");
}
if(contactFlowId === undefined){
throw new Error("Missing Amazon Connect Contact flow Id in the context");
}
if(pinpointAppId === undefined && smsNumber === undefined){
enableSMS = false;
} else if (pinpointAppId !== undefined && smsNumber === undefined){
throw new Error("Missing smsNumber in the context");
} else if (pinpointAppId === undefined && smsNumber !== undefined){
throw new Error("Missing pinpointAppId in the context");
} else {
enableSMS = true;
}
if(fbSecretArn === undefined){
enableFB = false;
} else {
enableFB = true;
}
if(enableFB === false && enableSMS === false){
throw new Error("Please enable at least one channel, SMS or Facebook. You can do so by providing fbSecretArn in the context to enable Facebook or by providing pinpointAppId and smsNumber to enable SMS channel");
}
const debugLog = new cdk.CfnParameter(this, 'debugLog', {
allowedValues: ['true', 'false'],
default: 'false',
type: 'String',
description:
'Setting to enable debug level logging in lambda functions. Recommended to turn this off in production.',
});
// pinpoint project will not be in cdk - phone number has to be manually claimed
// DDB - need GSI
// Dynamo DB table
const chatContactDdbTable = new dynamodb.Table(this, 'chatTable', {
partitionKey: {
name: 'contactId',
type: dynamodb.AttributeType.STRING,
},
timeToLiveAttribute: 'date',
billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
removalPolicy: cdk.RemovalPolicy.DESTROY,
});
// Dynamo DB table GSI
// vendorId is phone number or facebook user id
const vendorIdChannelIndexName = 'vendorId-index';
chatContactDdbTable.addGlobalSecondaryIndex({
indexName: vendorIdChannelIndexName,
partitionKey: {
name: 'vendorId',
type: dynamodb.AttributeType.STRING,
},
sortKey: {
name: 'channel',
type: dynamodb.AttributeType.STRING,
},
});
let smsOutboundMsgStreamingTopic;
let smsOutboundMsgStreamingTopicStatement;
if(enableSMS){
// outbound SNS topic
smsOutboundMsgStreamingTopic = new sns.Topic(
this,
'smsOutboundMsgStreamingTopic',
{}
);
smsOutboundMsgStreamingTopicStatement = new iam.PolicyStatement({
actions: [
'sns:Subscribe',
'sns:Publish'
],
principals: [new iam.ServicePrincipal('connect.amazonaws.com')],
resources: [smsOutboundMsgStreamingTopic.topicArn],
});
smsOutboundMsgStreamingTopic.addToResourcePolicy(smsOutboundMsgStreamingTopicStatement)
}
let digitalOutboundMsgStreamingTopic;
let digitalOutboundMsgStreamingTopicStatement;
if(enableFB){
digitalOutboundMsgStreamingTopic = new sns.Topic(
this,
'digitalOutboundMsgStreamingTopic',
{}
);
digitalOutboundMsgStreamingTopicStatement = new iam.PolicyStatement({
actions: [
'sns:Subscribe',
'sns:Publish'
],
principals: [new iam.ServicePrincipal('connect.amazonaws.com')],
resources: [digitalOutboundMsgStreamingTopic.topicArn],
});
digitalOutboundMsgStreamingTopic.addToResourcePolicy(digitalOutboundMsgStreamingTopicStatement);
}
// Inbound Lambda function
const inboundMessageFunction = new lambda.Function(
this,
'inboundMessageFunction',
{
runtime: lambda.Runtime.NODEJS_14_X,
handler: 'index.handler',
code: lambda.Code.fromAsset(
path.resolve(__dirname, '../src/lambda/inboundMessageHandler')
),
timeout: Duration.seconds(120),
memorySize: 512,
environment: {
FB_SECRET: fbSecretArn,
CONTACT_TABLE: chatContactDdbTable.tableName,
AMAZON_CONNECT_ARN: amazonConnectArn,
CONTACT_FLOW_ID: contactFlowId,
DIGITAL_OUTBOUND_SNS_TOPIC: (digitalOutboundMsgStreamingTopic !== undefined ? digitalOutboundMsgStreamingTopic.topicArn : "" ),
SMS_OUTBOUND_SNS_TOPIC: (smsOutboundMsgStreamingTopic !== undefined ? smsOutboundMsgStreamingTopic.topicArn : "" ),
VENDOR_ID_CHANNEL_INDEX_NAME: vendorIdChannelIndexName,
DEBUG_LOG: debugLog.valueAsString
},
}
);
// Inbound SNS topic (for SMS)
let inboundSMSTopic: sns.Topic;
if(enableSMS){
inboundSMSTopic = new sns.Topic(this, 'InboundSMSTopic', {});
inboundSMSTopic.addSubscription(
new subscriptions.LambdaSubscription(inboundMessageFunction)
);
new cdk.CfnOutput(this, 'SmsInboundTopic', {
value: inboundSMSTopic.topicArn.toString(),
});
}
if(enableFB){
inboundMessageFunction.addToRolePolicy(
new iam.PolicyStatement({
actions: ['secretsmanager:GetSecretValue'],
resources: [fbSecretArn],
effect: iam.Effect.ALLOW,
})
);
}
inboundMessageFunction.addToRolePolicy(
new iam.PolicyStatement({
actions: ['connect:StartChatContact'],
resources: [
`${this.node.tryGetContext("amazonConnectArn")}/contact-flow/${this.node.tryGetContext("contactFlowId")}`,
],
effect: iam.Effect.ALLOW,
})
);
inboundMessageFunction.addToRolePolicy(
new iam.PolicyStatement({
actions: ['connect:StartContactStreaming'],
resources: [`${this.node.tryGetContext("amazonConnectArn")}/contact/*`],
effect: iam.Effect.ALLOW,
})
);
inboundMessageFunction.addToRolePolicy(
new iam.PolicyStatement({
actions: [
'dynamodb:PutItem',
'dynamodb:GetItem',
'dynamodb:Scan',
'dynamodb:Query',
'dynamodb:UpdateItem',
],
resources: [
chatContactDdbTable.tableArn,
`${chatContactDdbTable.tableArn}/index/${vendorIdChannelIndexName}`,
],
effect: iam.Effect.ALLOW,
})
);
// SNS topic filter rules (filter by attribute at the topic level)
// outbound Lambda function
const outboundMessageFunction = new lambda.Function(
this,
'outboundMessageFunction',
{
runtime: lambda.Runtime.NODEJS_14_X,
handler: 'index.handler',
code: lambda.Code.fromAsset(
path.resolve(__dirname, '../src/lambda/outboundMessageHandler')
),
timeout: Duration.seconds(60),
memorySize: 512,
environment: {
CONTACT_TABLE: chatContactDdbTable.tableName,
PINPOINT_APPLICATION_ID: pinpointAppId,
FB_SECRET: fbSecretArn,
SMS_NUMBER: smsNumber,
DEBUG_LOG: debugLog.valueAsString,
},
}
);
outboundMessageFunction.addToRolePolicy(
new iam.PolicyStatement({
actions: ['mobiletargeting:SendMessages'],
effect: iam.Effect.ALLOW,
resources: [
`arn:aws:mobiletargeting:${this.region}:${this.account}:apps/${this.node.tryGetContext("pinpointAppId")}/messages`,
],
})
);
if(enableFB){
outboundMessageFunction.addToRolePolicy(
new iam.PolicyStatement({
actions: ['secretsmanager:GetSecretValue'],
resources: [fbSecretArn],
effect: iam.Effect.ALLOW,
})
);
}
outboundMessageFunction.addToRolePolicy(
new iam.PolicyStatement({
actions: ['dynamodb:GetItem', 'dynamodb:DeleteItem'],
resources: [
chatContactDdbTable.tableArn,
`${chatContactDdbTable.tableArn}/index/${vendorIdChannelIndexName}`,
],
effect: iam.Effect.ALLOW,
})
);
// health check Lambda
let healthCheckFunction: lambda.Function;
let digitalChannelMessageIntegration: apigw2i.HttpLambdaIntegration;
let digitalChannelHealthCheckIntegration: apigw2i.HttpLambdaIntegration;
let digitalChannelApi;
if(enableFB){
healthCheckFunction = new lambda.Function(
this,
'healthCheckFunction',
{
runtime: lambda.Runtime.NODEJS_14_X,
handler: 'index.handler',
code: lambda.Code.fromAsset(
path.resolve(__dirname, '../src/lambda/digitalChannelHealthCheck')
),
environment: {
DEBUG_LOG: debugLog.valueAsString,
FB_SECRET: fbSecretArn,
},
}
);
healthCheckFunction.addToRolePolicy(
new iam.PolicyStatement({
actions: ['secretsmanager:GetSecretValue'],
resources: [fbSecretArn],
effect: iam.Effect.ALLOW,
})
);
// inbound API Gateway (digital channel)
digitalChannelMessageIntegration = new apigw2i.HttpLambdaIntegration(
'inboundMessageFunction', inboundMessageFunction);
// digitalChannelHealthCheckIntegration = new apigw2i.HttpLambdaIntegration({
digitalChannelHealthCheckIntegration = new apigw2i.HttpLambdaIntegration(
'healthCheckFunction', healthCheckFunction);
digitalChannelApi = new apigw2.HttpApi(this, 'digitalChannelApi', {
corsPreflight: {
allowOrigins: ['*'],
allowMethods: [
apigw2.CorsHttpMethod.OPTIONS,
apigw2.CorsHttpMethod.POST,
apigw2.CorsHttpMethod.GET,
],
allowHeaders: ['Content-Type'],
},
});
digitalChannelApi.addRoutes({
path: '/webhook/facebook',
methods: [apigw2.HttpMethod.POST],
integration: digitalChannelMessageIntegration,
});
digitalChannelApi.addRoutes({
path: '/webhook/facebook',
methods: [apigw2.HttpMethod.GET],
integration: digitalChannelHealthCheckIntegration,
});
new cdk.CfnOutput(this, 'FacebookApiGatewayWebhook', {
value: digitalChannelApi.apiEndpoint.toString() + '/webhook/facebook',
});
// Outbound lambda subscribe to streaming topic
if(digitalOutboundMsgStreamingTopic){
digitalOutboundMsgStreamingTopic.addSubscription(
new subscriptions.LambdaSubscription(outboundMessageFunction, {
filterPolicy: {
MessageVisibility: sns.SubscriptionFilter.stringFilter({
allowlist: ['CUSTOMER', 'ALL'],
}),
}
})
);
}
}
if(smsOutboundMsgStreamingTopic){
smsOutboundMsgStreamingTopic.addSubscription(
new subscriptions.LambdaSubscription(outboundMessageFunction, {
filterPolicy: {
MessageVisibility: sns.SubscriptionFilter.stringFilter({
allowlist: ['CUSTOMER', 'ALL'],
}),
}
})
);
}
}