James Thomas

Notes on JavaScript

Building an SMS Bot for Slack.

This is smsbot.

It provides an integration with Slack that connects SMS messages into channels. People can text an external number and have their messages posted into the channel. Channel users can respond to the messages and have their response sent back to the sender using SMS.

smsbot was developed in under a few hours and less than one hundred lines of code using a serverless cloud platform.

Want to understand how it works? Let’s find out…

The first challenge was how to programmatically send and receive SMS messages.

Want to deploy smsbot yourself? Follow the instructions at the bottom or check out the Github repository.

Twilio

Twilio provides a platform for building SMS, voice and messaging applications using an API.

Developers can register phone numbers through the service that invoke webhooks for incoming calls and SMS messages. Webhooks are passed message details and return a custom markup language (TwilML) to encode the instructions on how to respond to the request.

The platform also provides a REST API to initiate phone calls and SMS messages.

We now have a way to handle text messages for our bot, how do we integrate a new bot in Slack?

Slack

Slack also provides a webhook-based mechanism to integrate custom bots into the platform. The platform has two different integrations…

Incoming Webhooks.

Provide a way to post messages into Slack from external sources. It provides a custom URL that supports HTTP requests with a JSON payload. These requests are turned into channel messages. The JSON payload is used to control the content and formatting of the message.

https://api.slack.com/incoming-webhooks

Outgoing Webhooks

Allow you to listen for messages in channels without using the full real-time API. Slack sends HTTP requests to registered URLs when specific trigger words appear in channel messages. The HTTP request body contains the message details.

https://api.slack.com/outgoing-webhooks

Now we just need a way to write simple HTTP services to listen for webhook requests…

OpenWhisk

OpenWhisk is an open-source serverless cloud platform. Serverless platforms make it easy to create microservices in the cloud without having to set up or manage any infrastructure.

Developers push their code directly into the platform. The platform will instantiate the runtime and invoke the code on-demand for each request. Serverless functions can be exposed as HTTP endpoints or connected to event sources like message queues or databases.

Serverless platforms make it easy to create simple HTTP services to handle webhook requests.

Web Actions

Web Actions are a new feature in OpenWhisk for exposing serverless functions as simple HTTP endpoints. Functions have access to the full HTTP request and can control the HTTP response returned. This method is suitable for simple public endpoints that do not need more enterprise features supported by the API gateway.

Web Actions are available at the following platform API path.

1
https://{APIHOST}/api/v1/experimental/web/{USER_NAMESPACE}/{PACKAGE}/{ACTION_NAME}.{TYPE}
  • APIHOST - platform endpoint e.g. openwhisk.ng.bluemix.net.
  • USER_NAMESPACE - must be explicit and cannot use the default namespace (_).
  • PACKAGE - action package or default.
  • ACTION_NAME - function identifier.
  • TYPE - .json, .html, .text or .http.

Example

Here’s a simple Web Actions that returns HTML content when invoked through the API.

1
2
3
4
5
6
7
8
function main(args) {
    var msg = "you didn't tell me who you are."
    if (args.name) {
        msg = `hello ${args.name}!`
    }
    return {body:
       `<html><body><h3><center>${msg}</center></h3></body></html>`}
}

Actions can be turned into web-accessible actions by setting a custom annotation.

1
$ wsk action create greeting source.js --annotation web-export true

The greeting function can then be invoked through a HTTP request to the following endpoint.

https://openwhisk.ng.bluemix.net/api/v1/experimental/web/user@host.com_dev/default/greeting.http?name=James

1
2
3
4
5
$ http post https://openwhisk.ng.bluemix.net/api/v1/experimental/web/user@host.com_dev/default/html_greeting.http?name=James
HTTP/1.1 200 OK
...

<html><body><h3><center>hello James!</center></h3></body></html>

Twilio <=> Slack

OpenWhisk Web Actions are a great solution for creating webhook endpoints. Connecting Twilio to Slack (and vice-versa) can be implemented using two different OpenWhisk Web Actions.

  • Twilio Webhook. Invoked for SMS messages. Uses the Slack Incoming Webhook to create a bot messages from content. ​
  • Slack Outgoing Webhook. Invoked for channel replies. Uses Twilio API to send replies as SMS messages.

Let’s have a look at the Twilio webhook first…

Twilio Webhook

When a new SMS message is received, we want to post this bot message into our Slack channel.

Twilio allows developers to configure webhooks for each registered phone number. The webhook endpoint will be invoked for each SMS message that is received. Twilio can either send a HTTP POST request, with parameters in the body, or a HTTP GET request, with URL query parameters.

OpenWhisk Web Actions support both formats. Request parameters will be available as properties on the function argument.

Here’s a simple Web Action that would log the message sender and content for each SMS received.

1
2
3
function main (params) {
  console.log(`Text message from ${params.From}: ${params.Body}`)
}

Creating Bot Messages From SMS

When an SMS message is received, we need to send a HTTP POST to the Incoming Webhook URL. The JSON body of the HTTP request is used to configure the channel message. Using the username, icon_emoji and and text properties allows us to customise our bot message.

OpenWhisk Actions in Node.js have numerous popular NPM modules pre-installed in the environment. This includes a HTTP client library. This code snippet demonstrates sending the HTTP request to create out bot message. The Slack Webhook URL is bound as a default parameter to the action.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const request = require('request')

const slack_message = text => ({
  username: 'smsbot',
  icon_emoji: ':phone:',
  text
})

function main (params) {
  return new Promise(function (resolve, reject) {
    request.post({
      body: slack_message(`Text message from ${params.From}: ${params.Body}`),
      json: true,
      url: params.slack.webhook
    }, err => {
      if (err) return reject(err);
      resolve();
    })
  })
}

Returning a Promise ensures the request is completed before the function exits.

Sending Acknowledgement Message

Returning TwilML content allows us to control the response from Twilio to the incoming message.

This snippet would send an SMS reply to sender with the content “Hello World!”.

1
2
3
4
<?xml version="1.0" encoding="UTF-8"?>
<Response>
  <Message>Hello World!</Message>
</Response>

Twilio’s client library for Node.js can be used to programatically generate TwilML.

1
2
3
const twilio = require('twilio')
const resp = new twilio.TwimlResponse()
const twilml = resp.message('Thanks for letting us know.').toString()

Returning XML content as the HTTP response requires us to set the response headers, body and status code in the Web Action.

1
2
3
4
5
6
7
8
9
10
function main () {
  const xml = '...'
  return {
    headers: {
      'Content-Type': 'text/xml'
    },
    code: 200,
    body: xml
  }
}

Web Action Source

Adding the XML response code into the existing function completes the OpenWhisk Web Action required to handle incoming SMS messages.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
const request = require('request')
const twilio = require('twilio')

const resp = new twilio.TwimlResponse()
const twilml = resp.message('Thanks for letting us know.').toString()

const response = {
  headers: {
    'Content-Type': 'text/xml'
  },
  code: 200,
  body: twilml
}

const slack_message = text => ({
  username: 'smsbot',
  icon_emoji: ':phone:',
  text
})

function main (params) {
  return new Promise(function (resolve, reject) {
    request.post({
      body: slack_message(`Text message from ${params.From}: ${params.Body}`),
      json: true,
      url: params.slack.webhook
    }, err => {
      if (err) return reject(err);
      resolve(response);
    })
  })
}

Register Webhook

Once we have deployed the Web Action, we can configure the Twilio SMS webhook endpoint to use following URL.

https://openwhisk.ng.bluemix.net/api/v1/experimental/web/user@email.com_dev/default/smsbot-dev-incoming.http

Slack Outgoing Webhook

When someone sends a channel message to the bot, smsbot should send that content as an SMS message to the last person who sent an SMS to the channel. An Outgoing Webhook will be used to trigger the bot.

Outgoing Webhooks have a configurable trigger word. Channel messages which start with this word are send as HTTP requests to the list of URLs registered for that webhook. We will use smsbot as our trigger word.

Request Parameters

Slack sends the following parameters for each channel message.

1
2
3
4
5
6
7
8
9
10
token=XXXXXXXXXXXXXXXXXX
team_id=T0001
team_domain=example
channel_id=C2147483705
channel_name=test
timestamp=1355517523.000005
user_id=U2147483697
user_name=Steve
text=googlebot: What is the air-speed velocity of an unladen swallow?
trigger_word=googlebot:

In OpenWhisk Web Actions, these parameters will be available on the function argument object.

Here’s a simple Web Action that would parse and log the message contents when the webhook is fired.

1
2
3
4
function main (params) {
  const msg = params.text.slice(params.trigger_word.length + 1)
  console.log('channel message:', msg)
}

When this webhook is fired, we need to add code to send an SMS message with the channel message.

Sending SMS Messages

Twilio’s API allows us to programatically send SMS messages from our registered numbers.

This snippet shows you how to use their Node.js client library to send sample message.

1
2
3
4
5
6
7
8
9
10
const twilio = require('twilio')
const creds = { account: '...', auth: '...' }

const client = twilio(creds.account, creds.auth)
const callback = (err, message) => {
  if (err) return console.log(err)
  console.log('sent sms message.')
}

client.messages.create({ to: '...', from: '...', body: 'hello world' }, callback)

The webhook should use this client library to send a message to the last person who send us an incoming message.

Reply to Message Sender

How can we determine who was the last person who sent a message to our bot? The Web Action processing the incoming messages is a separate service to the Web Action sending SMS messages.

Rather than setting up a database to share application state, the service can use Twilio’s API to retrieve the received message details.

1
2
3
4
5
6
7
8
const twilio = require('twilio')
const creds = { account: '...', auth: '...' }
const client = twilio(creds.account, creds.auth)

client.messages.list({to: '+44....'}, (err, data) => {
  const last = data.messages[0]
  console.log(`last message from: ${last.from}`)
})

SMSBot Channel Response

Outgoing Webhooks which respond with a JSON body will generate a new channel message.

1
2
3
4
5
{
  "username": "smsbot",
  "icon_emoji": ":phone:",
  "text": "sms sent to ..."
}

Web Action Source

Combing the channel message parsing code with the snippets for sending SMS messages and obtaining the last message sender completes the Web Action needed to handle the Outgoing Webhook.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const twilio = require('twilio')

const slack_message = text => ({
  username: 'smsbot',
  icon_emoji: ':phone:',
  text
})

function reply (params) {
  const client = twilio(params.twilio.account, params.twilio.auth)
  return new Promise((resolve, reject) => {
    client.messages.list({to: params.twilio.number}, (err, data) => {
    if (err) return Promise.reject(err)

    const last = data.messages[0]
    const msg = params.text.slice(params.trigger_word.length + 1)
    const options = { to: last.from, from: last.to, body: msg }
    client.messages.create(options, (err, message) => {
        if (err) return Promise.reject(err)
        resolve(slack_message(`sms reply sent to ${last.from}`))
      })
    })
  })
}

Twilio account credentials are bound as default parameters to the Web Action during deployment.

Deployment

smsbot is built using The Serverless Framework.

This framework makes building serverless applications really easy. The tool handles the entire configuration and deployment process for your serverless provider. OpenWhisk recently released integration with the framework through a provider plugin.

Let’s look at how to use the framework to deploy our serverless application…

OpenWhisk

Register for an account with an OpenWhisk provider, e.g. IBM Bluemix.

Set up the wsk CLI and run the command to authenticate against the platform endpoint.

1
wsk property set --apihost openwhisk.ng.bluemix.net --auth SECRET

Serverless Framework

Install the The Serverless Framework and the OpenWhisk provider plugin.

1
npm install --global serverless serverless-openwhisk

Source Code

Download the source code from Github and install the project dependencies.

1
2
3
$ git clone https://github.com/ibmets/smsbot.git
$ cd smsbot
$ npm install

Create a new file called credentials.yml with the following content.

1
2
3
4
5
6
7
twilio:
    account:
    auth:
    number:
numbers:
slack:
    webhook:

Twilio

Register an account with Twilio and provision a new phone number. Make a note of the phone number. Retrieve the account identifier and auth token from the Twilio console.

Fill in the account identifier, auth token and phone number in the credentials.yml file.

1
2
3
4
twilio:
    account: AC_USER_ID
    auth: AUTH_TOKEN
    number: '+441234567890'

Important: the twilio.number property value must be a quoted string.

Phone Numbers

During Twilio’s free trial, you will need manually verify each phone number that you want to send messages to.

Fill in all verified numbers in credentials.yml.

1
2
3
numbers:
    '+441234567890': Joe Smith
    '+441234567891': Jane Smith

Important: the numbers property values must be a quoted strings.

Incoming Webhook

Create a new Incoming Webhook integration for the Slack channel messages should appear in.

Fill in the slack.webhook property in credentials.yml with this url.

1
2
slack:
    webhook: https://hooks.slack.com/services/XXXX/YYYY/ZZZZ

Deploy Application

Use The Serverless Framework to deploy your application.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
$ serverless deploy
Serverless: Packaging service...
Serverless: Compiling Functions...
Serverless: Compiling API Gateway definitions...
Serverless: Compiling Rules...
Serverless: Compiling Triggers & Feeds...
Serverless: Deploying Functions...
Serverless: Deployment successful!

Service Information
platform:   openwhisk.ng.bluemix.net
namespace:  _
service:    smsbot

actions:
smsbot-dev-incoming    smsbot-dev-reply

triggers:
**no triggers deployed**

rules:
**no rules deployed**

endpoints:
**no routes deployed**

Twilio Webhook

On the Phone Numbers page in the Twilio console, configure the ”Messaging” webhook URL.

Use this Web Action URL, replacing user@host.com_dev with your namespace.

https://openwhisk.ng.bluemix.net/api/v1/experimental/web/user@host.com_dev/default/smsbot-dev-incoming.http

Outgoing Webhook

Create a new Outgoing Webhook integration for the Slack channel messages should appear in. Use smsbot as the Trigger Word.

Use this Web Action URL, replacing user@host.com_dev with your namespace.

https://openwhisk.ng.bluemix.net/api/v1/experimental/web/user@host.com_dev/default/smsbot-dev-reply.json

Test it out!

Send a text message to the phone number you registered through Twilio. smsbot should post the contents into Slack and send an SMS response with the message ”Thanks for letting us know!”.

If you send a channel message starting with the trigger word (smsbot), the phone number should receive a new SMS message with the message text.

Awesome-sauce 😎.

Conclusions

OpenWhisk Web Actions provide a convenient way to expose serverless functions as simple HTTP APIs. This feature is ideal for implementing webhook endpoints.

Both Slack and Twilio provide webhook integration for developers to use their platforms. Using OpenWhisk Web Actions, we can write serverless functions that act as a bridge between these services. With less than a hundred lines of code, we’ve created a new slack bot that can connect users to channels using SMS messages.

Pretty cool, huh?! 👏👏👏

Comments