AWS CloudFormation Lambda Node.js Twilio Function – Part 3

A

In Part 1 and Part 2 of this walk through, I built a simple AWS Lambda function in Node to accept a form post. I this post, the API endpoint will be modified to accept a SMS message from Twilio, save the message to a DynamoDB and respond with an SMS message.

If you are just interested in the code, that can be found on my GitHub Repository.

Update the API Gateway

AWS API Gateway is designed to accept and respond with JSON data as the standard response. Unfortunately, Twilio submits POST requests as URL-Encoded Forms (x-www-form-urlencoded) and reads XML responses, so I have customize API Gateway to work in a way that can work with Twilio. To do this I’ve added another resource and modified the event of the HandleTwilioFunction to the template.json from Part 2.

The first important section is for processing the incoming form and attaching it to the event JSON object in the attribute “form”:

"application/x-www-form-urlencoded": "{\"form\" : $input.json(\"$\")}"

The second important section is responding with “application/xml” in the Content-Type header.

"responses": {
  "default": {
    "statusCode": 200,
    "responseTemplates": {
      "application/xml": "$input.path(\"$\").body"
    },
    "responseParameters": {
      "method.response.header.Content-Type": "'application/xml'"
    }
  }
}

Below is an extended version (but still incomplete) of the template.json

{
  "Resources": {
    ...
    "HandleTwilioFunction": {
      ...
      "Properties": {
        ...
        "Events": {
          "PostResource": {
            "Type": "Api",
            "Properties": {
              "Path": "/twilio",
              "Method": "post",
              "RestApiId": {
                "Ref": "TwilioAPIGateway"
              }
            }
          }
        }
      }  
    },

    ...

    "TwilioAPIGateway": {
      "Type": "AWS::Serverless::Api",
      "Properties": {
        "StageName": "Prod",
        "DefinitionBody": {
          "swagger": 2.0,
          "info": {
            "title": {
              "Ref": "AWS::StackName"
            }
          },
          "paths": {
            "/twilio": {
              "post": {
                "consumes": [
                  "application/x-www-form-urlencoded"
                ],
                "produces": [
                  "application/xml"
                ],
                "parameters": [
                  {
                    "required": true,
                    "in": "body",
                    "name": "Empty",
                    "schema": {
                      "$ref": "#/definitions/Empty"
                    }
                  }
                ],
                "responses": {
                  "200": {
                    "headers": {
                      "Content-Type": {
                        "type": "string"
                      }
                    },
                    "description": "200 response",
                    "schema": {
                      "$ref": "#/definitions/Empty"
                    }
                  }
                },
                "x-amazon-apigateway-integration": {
                  "httpMethod": "POST",
                  "passthroughBehavior": "when_no_match",
                  "requestTemplates": {
                    "application/x-www-form-urlencoded": "{\"form\" : $input.json(\"$\")}"
                  },
                  "responses": {
                    "default": {
                      "statusCode": 200,
                      "responseTemplates": {
                        "application/xml": "$input.path(\"$\").body"
                      },
                      "responseParameters": {
                        "method.response.header.Content-Type": "'application/xml'"
                      }
                    }
                  },
                  "type": "aws",
                  "uri": {
                    "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${HandleTwilioFunction.Arn}/invocations"
                  }
                }
              }
            }
          },
          "definitions": {
            "Empty": {
              "type": "object",
              "title": "Empty Schema"
            }
          }
        }
      }
    }
  }
}

Update the Lambda Function

The Lambda is modified slightly to handle the URL-Encoded Form as well as return an XML response. Since, this is such a simple example I am just returning XML directly. Twilio does maintain an NPM library for more advanced usage, but I won’t need that with this example.

'use strict';

const AWS = require('aws-sdk'),
    dynamo = new AWS.DynamoDB.DocumentClient(),
    crypto = require('crypto'),
    querystring = require('querystring'),
    tableName = process.env.TABLE_NAME;

const createResponse = function(statusCode, body) {
    return {
        statusCode: statusCode,
        body: body
    };
};

// https://stackoverflow.com/questions/105034/create-guid-uuid-in-javascript
const uuidv4 = function() {
    return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
        var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
        return v.toString(16);
    });
};

exports.handleTwilio = function(event, context, callback) {
    let form;
    try {
        form = querystring.parse(event.form);
    } catch(err) {
        callback(null, createResponse(400, 'Unable to Parse Form'));
    }
    if (form) {
        let itm = Object.keys(form).reduce((itm, key) => {
            if (form[key] !== '') { // eliminate empty string values (DynamoDB does not allow them)
                itm[key] = form[key];
            }
            return itm;
        }, {
            id: uuidv4()
        });

        const params = {
            TableName: tableName,
            Item: itm
        };

        dynamo.put(params).promise()
        .then((data) => {
            // success
            // Twilio Response Message: https://www.twilio.com/docs/api/twiml/sms/your_response
            if (itm.Body) {
                callback(null, createResponse(200, `You sent the message: ${itm.Body}`));
            } else {
                callback(null, createResponse(200, `I'm not sure what message you sent.`));
            }
            return;
        })
        .catch(function(err) {
            // failure
            console.log(err);
            callback(null, createResponse(500, 'Failure'));
        });
    } else {
        callback(null, createResponse(400, 'Bad Request'));
    }
};

Once again, package and deploy this code.

aws cloudformation package \
    --template-file template.json \
    --s3-bucket S3_Bucket_Name \
    --output-template-file packaged-template.json \
    --use-json

aws cloudformation deploy \
    --template-file packaged-template.json \
    --stack-name your-stackname-here \
    --region us-west-2 \
    --capabilities CAPABILITY_IAM

Set Twilio to use API Gateway

Now that the API Gateway and Lambda function have been deployed, the last step is to update Twilio to use the API Gateway. The URL is something like  https://my-api-id.execute-api.us-west-w.amazonaws.com/Prod/twilio (see Invoking an API in Amazon API Gateway for more details). In the Phone Numbers section of the Twilio Dashboard, select the desired phone number and in the Messaging Section, enter your API Gateway URL.

Twilio SMS Webhook URL Settings

Finishing Up

Once Twilio  is updated, the Lambda function will begin responding to incoming SMS messages and storing the requests in a DynamoDB. Right now, it isn’t very useful, but it works and can be easily extended to serve many purposes.

About the author

By mark

Recent Posts

Categories