Skip to content

Commit

Permalink
Add a small example with a custom Twilio component
Browse files Browse the repository at this point in the history
  • Loading branch information
ellismg committed Jun 9, 2018
1 parent 710a2ec commit 0cf7392
Show file tree
Hide file tree
Showing 6 changed files with 266 additions and 0 deletions.
3 changes: 3 additions & 0 deletions twilio-ts-component/Pulumi.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
name: twilio-ts-component
description: A small example that builds a Pulumi component for Twilio Programable SMS
runtime: nodejs
79 changes: 79 additions & 0 deletions twilio-ts-component/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# Twilio SMS Handler

A sample for interacting with Twilio SMS. This sample includes a custom Component Resource that abstracts the tedium of interacting with API Gateway and parsing incoming messages from Twilo. This sample requires you to have a Twilio number which can handle SMS.

## Deploying and running the program

1. Create a new stack:

```
$ pulumi stack init twilio-test
```

1. Set the AWS region:

```
$ pulumi config set aws:region us-west-2
```

1. Configure Twilio settings

```
$ pulumi config set twilio:accountSid <your account sid from https://www.twilio.com/console>
$ pulumi config set --secret twilio:authToken <your auth token from https://www.twilio.com/console>
$ pulumi config set phoneNumberSid <the phone number sid from https://www.twilio.com/console/phone-numbers/>
```

1. Restore NPM modules via `npm install`.

1. Compile the program via `tsc` or `npm run build`.

1. Preview and run the deployment via `pulumi update`.
```
$ pulumi update
Previewing update of stack 'url-shortener-dev'
...
Updating stack 'twilio-dev'
Performing changes:
Type Name Status Info
+ pulumi:pulumi:Stack aws-serverless-twilio-component-twilio-dev created
+ ├─ twilio:rest:IncomingPhoneNumber twilio-example created
+ │ └─ aws-serverless:apigateway:API twilio-example-api created
+ │ ├─ aws:apigateway:RestApi twilio-example-api created
+ │ ├─ aws:apigateway:Deployment twilio-example-api created
+ │ ├─ aws:lambda:Permission twilio-example-api-c9e56dfd created
+ │ └─ aws:apigateway:Stage twilio-example-api created
+ └─ aws:serverless:Function twilio-example-apic9e56dfd created
+ ├─ aws:iam:Role twilio-example-apic9e56dfd created
+ ├─ aws:iam:RolePolicyAttachment twilio-example-apic9e56dfd-32be53a2 created
+ └─ aws:lambda:Function twilio-example-apic9e56dfd created
---outputs:---
smsUrl: "https://k44yktdqf8.execute-api.us-west-2.amazonaws.com/stage/sms"
info: 11 changes performed:
+ 11 resources created
Update duration: 27.155440706s
```

1. Send an SMS message to the phone number you have registered with Twilio, or make a request by hand with cURL (you may wish to pass aditional data with your request, see https://www.twilio.com/docs/sms/twiml#request-parameters for the complete set of data that Twilio sends).

```
$ curl -X POST -d "From=+12065555555" -d "Body=Hello!" $(pulumi stack output smsUrl)
```

## Clean up

To clean up resources, run `pulumi destroy` and answer the confirmation question at the prompt.

## About the code

This example builds and uses a custom `pulumi.CustomResource` to make it easy to spin up a SMS handler on Twilio. It could be extended to support Voice as well, by adding an additional handler to `twilio.IncomingPhoneNumberArgs`.

The custom resource itself is in [`twilio.ts`](./twilio.ts) and handles the work to use `@pulumi/aws-serverless` to create a REST endpoint with `serverless.apigateway.API`. The handler registered with API Gateway does some of the teadious work of decoding the incoming event data and the delegates to the actual handler provided to the custom resource.

In addition, at deployment time, the custom resource uses the Twilio SDK to update the SMS Handler for the provided phone number, instead of forcing you to register it by hand in the Twilio console.

Twilio can handle either responses with `text/plain` or `application/xml` Content-Types (when `application/xml` is used, Twilio treats the response as TwiML). `serverless.apigateway.API` defaults to `application/json`, which will cause Twilio to fail to process the response, so we explicitly set the Content-Type header to `text/plain` in this example.
27 changes: 27 additions & 0 deletions twilio-ts-component/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import * as pulumi from "@pulumi/pulumi";
import * as twilo from "./twilio"

const config = new pulumi.Config(pulumi.getProject());
const phoneNumberSid = config.require("phoneNumberSid");

const handler = new twilo.IncomingPhoneNumber("twilio-example", {
phoneNumberSid: phoneNumberSid,
handler: async (p) => {
return {
statusCode: 200,
headers: {
"Content-Type": "text/plain"
},
body: `Made with \u2764 and Pulumi.`
}
}
});

// We export the SMS URL, for debugging, you can post messages to it with curl to test out your handler without
// having to send an SMS. For example:
//
// $ curl -X POST -d "From=+12065555555" -d "Body=Hello!" $(pulumi stack output smsUrl)
//
// There are many additional properties you can provide which will be decoded and presented to your handler,
// see: https://www.twilio.com/docs/sms/twiml#request-parameters
export let smsUrl = handler.smsUrl;
17 changes: 17 additions & 0 deletions twilio-ts-component/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"name": "twilio-ts-component",
"main": "bin/index.js",
"typings": "bin/index.d.ts",
"scripts": {
"build": "tsc"
},
"devDependencies": {
"typescript": "^2.7.2",
"@types/node": "latest"
},
"dependencies": {
"@pulumi/pulumi": "^0.12.3",
"@pulumi/aws-serverless": "dev",
"twilio": "latest"
}
}
24 changes: 24 additions & 0 deletions twilio-ts-component/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"compilerOptions": {
"outDir": "bin",
"target": "es6",
"lib": [
"es6"
],
"module": "commonjs",
"moduleResolution": "node",
"declaration": true,
"sourceMap": true,
"stripInternal": true,
"experimentalDecorators": true,
"pretty": true,
"noFallthroughCasesInSwitch": true,
"noImplicitAny": true,
"noImplicitReturns": true,
"forceConsistentCasingInFileNames": true,
"strictNullChecks": true
},
"files": [
"index.ts"
]
}
116 changes: 116 additions & 0 deletions twilio-ts-component/twilio.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import * as pulumi from "@pulumi/pulumi";
import * as serverless from "@pulumi/aws-serverless";
import * as url from "url";
import { Callback } from "@pulumi/aws-serverless/function";
import { APIArgs } from "@pulumi/aws-serverless/api";

const config = new pulumi.Config("twilio");
const accountSid = config.require("accountSid");
const authToken = config.require("authToken");

export class IncomingPhoneNumber extends pulumi.ComponentResource {
public /*out*/ readonly smsUrl: pulumi.Output<string>;

constructor(name: string, args: IncomingPhoneNumberArgs, opts? : pulumi.ResourceOptions) {
super("twilio:rest:IncomingPhoneNumber", name, {}, opts);

const apiArgs : APIArgs = {
routes: [{
path: "/sms",
method: "POST",
handler: (e, ctx, cb) => {
// Twilio passes information to POST requests as application/x-www-form-urlencoded, so we decode
// the body of the request to get at it.
const qs = require("querystring");
const params = qs.parse(e.isBase64Encoded ? Buffer.from(e.body, "base64").toString() : e.body);

// Loop over any provided media and add it to our array.
const allMedia: MediaInfo[] = []
for (let i = 0; i < params.NumMedia; i++) {
allMedia.push({
ContentType: <string>params[`MediaContentType${i}`],
Url: <string>params[`MediaContentUrl${i}`]
})
}

// Copy the payload of the request into our representation.
const payload: SmsPayload = {
MessageSid: params.MessageSid,
AcountSid: params.AccountSid,
MessagingServiceSid: params.MessagingServiceSid,
From: params.From,
To: params.To,
Body: params.Body,
Media: allMedia,
FromLocation: {
City: params.FromCity,
State: params.FromState,
Zip: params.FromZip,
Country: params.FromCountry,
},
ToLocation: {
City: params.ToCity,
State: params.ToState,
Zip: params.ToZip,
Country: params.ToCountry,
}
}

// Delegate to the user provided handler.
return args.handler(payload, ctx, cb);
}
}]
};

const api = new serverless.apigateway.API(`${name}-api`, apiArgs, { parent: this });
this.smsUrl = api.url.apply(url => `${url}sms`);

// Use the twilio SDK to update the handler for the SMS webhook to what we just created.
const twilio = require("twilio")
const client = new twilio(accountSid, authToken);

this.smsUrl.apply(url =>{
client.incomingPhoneNumbers(args.phoneNumberSid).update({
smsMethod: "POST",
smsUrl: `${url}`
}).done();
});

// Register the smsUrl as an output of the component itself.
super.registerOutputs({
smsUrl: this.smsUrl
});
}
}

export interface IncomingPhoneNumberArgs {
phoneNumberSid: string,
handler: Callback<SmsPayload, serverless.apigateway.Response>
}

// See https://www.twilio.com/docs/sms/twiml#request-parameters for more information about
// what each parameter means.
export interface SmsPayload {
MessageSid: string,
AcountSid: string,
MessagingServiceSid: string,
From: string,
To: string,
Body: string,
Media: MediaInfo[];
FromLocation: Location,
ToLocation: Location,
}

// Twilio attempts to look up this information and provide it, but it may not always be present.
export interface Location {
City?: string,
State?: string,
Zip?: string,
Country?: string,
}

export interface MediaInfo {
ContentType: string,
Url: string,
}

0 comments on commit 0cf7392

Please sign in to comment.