Using Twilio Functions with ClojureScript

September 10, 2019
Written by

Blog post header: Using Twilio Functions with ClojureScript

Twilio offers several APIs for programmable communication which rely on webhooks. Twilio Functions is a serverless runtime which can handle these webhooks by running your code on Twilio’s platform to generate customized responses to phone calls, text or WhatsApp messages, emails and more - without having to manage a server yourself. Functions supports JavaScript, and therefore any of the compile-to-js languages can be used too, such as TypeScript.

Here I’ll show how you can use Shadow CLJS, the Twilio CLI and Serverless Toolkit to create a Twilio Function in ClojureScript. If you want to jump straight to the final code, you can find the project on GitHub.

Prereqs

Howto

Project Setup

Create a new Shadow CLJS project using npx which is part of the Node.js distribution:

npx create-cljs-project cljs-twilio-function

This creates a project using Shadow CLJS, in a directory called cljs-twilio-function.

Creating a Function

Twilio Functions in JavaScript look like this:

exports.handler = function(context, event, callback) {
  // your code here
  callback( null, “Your Response Here” );
}

So first create a ClojureScript namespace with a function which takes those three arguments and generates a response, then configure shadow-cljs to set up the export correctly.

Create a file in src/main called my_twilio_function.cljs, with the following content:

(ns my-twilio-function)

(defn my-function [context event callback]
  (callback nil "This is a ClojureScript Function!"))

(Note “my twilio function” has underscores the the filename and dashes in the code - this is a quirk of the Clojure compiler).

That’s all the code needed for now, so move on to configuring Shadow to build this into the appropriate JavaScript.  The shadow config file is shadow-cljs.edn. Edn is Clojure’s Extensible Data Notation, which is a subset of Clojure syntax. Change the empty map {} under the :builds key to define how the build should work:

:builds
 {:function
  {:target :node-library
   :exports {:handler my-twilio-function/my-function}
   :output-to "functions/my-function.js"
   :compiler-options {:optimizations :advanced}}}

This configuration:

  • defines a build called function which builds the code as a Node.js library
  • exports my-function as exports.handler
  • sets the output file to functions/my-function.js

Using :optimizations :advanced enables a host of optimizations that shrink non-public names, inline functions, remove unused code and produce minified JavaScript in a single file. You can check that your shadow-cljs.edn matches mine on GitHub.

Building and Running our Function

Compile the function with npx shadow-cljs release function. The first time this runs it might cause some dependencies to be downloaded but this won’t happen in future builds:

$ npx shadow-cljs release function
shadow-cljs - config: /home/mjg/code/twilio-cljs-function/cljs-twilio-function/shadow-cljs.edn  cli version: 2.8.52  node: v11.15.0
[:function] Compiling ...
[:function] Build completed. (23 files, 3 compiled, 0 warnings, 9.81s)

This creates the file functions/my-function.js which contains the function as highly-minified (and therefore quite unreadable) JavaScript.

Before deploying to the Twilio runtime, it’s possible to test the function locally using the Twilio CLI with the Serverless Toolkit. If these aren’t installed already, do so with:

npm install -g twilio-cli
twilio plugins:install @twilio-labs/plugin-serverless

Then run the function locally with twilio serverless:start. The output will show that the function is available at http://localhost:3000/my-function. The name of the function is determined by the name of the js file, so can be customized in shadow-cljs.edn.

Browser screenshot: This is a ClojureScript Function!

If the twilio serverless:start process is left running it will pick up any changes to existing functions when the JavaScript is recompiled. For now, I’d say that looks good, so move on to deploying the function to Twilio’s Serverless runtime.

Doing this needs a Twilio account. If you don’t have one you can sign up for free at twilio.com/try-twilio. From your account console you can find your Account SID and Auth Token which you will need next:

twilio login
twilio serverless:deploy

The serverless:deploy command will create a Twilio Function, upload our code and create a public URL which we can call to invoke our code:

Screenshot of the output of "twilio serverless:deploy"

🎉🎉 You did it! 🎉🎉 As shown near the end of the output from twilio serverless:deploy, the function is available publicly at https://cljs-twilio-function-XXXX-dev.twil.io/my-function.

Using shadow-cljs we have compiled a ClojureScript function to JavaScript, set up the exports correctly, tested locally and deployed to the Twilio Serverless Runtime. Congratulations, it's time for a well-earned cup of tea ☕.

 

A Better Function

Lets modify the function so that it can respond to an SMS with a random Rich Hickey quote. To prevent abuse we should also configure it so that the function can only be invoked by Twilio webhook requests.

Responding to SMS

Webhooks called by Twilio are expected to respond using TwiML, which is a dialect of XML. Unfortunately the built-in XML tools in ClojureScript rely on the DOM APIs, which are only available in browser runtimes. No matter, we can use hiccups instead. You may be familiar with hiccups as a tool for creating html, but it can be easily (ab)used to generate any kind of XML. First, add it to the :dependencies section of shadow-cljs.edn:

 :dependencies 
    [[hiccups "0.3.0"]]

Then change the code in the my-twilio-function namespace to use hiccups to generate TwiML (or copy-paste from the version on my GitHub). Some JavaScript interop is needed to set the content-type header correctly. If you’re not sure about the interop, Rafael Spacjer’s blog post is my go-to resource, along with this cheatsheet.

(ns my-twilio-function
  (:require-macros [hiccups.core :as hiccups :refer [html]])
  (:require [hiccups.runtime :as hiccupsrt]))

(defn my-function [context event callback]
 
  (callback nil
        (doto (new js/Twilio.Response)
          (.appendHeader "content-type" "application/xml")
          (.setBody (html [:Response
                           [:Message "This is a ClojureScript Function!"]])))))

As before, run npx shadow-cljs release function and twilio serverless:start (or perhaps you left that running earlier?) then load http://localhost:3000/my-function to see that the correct TwiML is being generated:

<Response>
    <Message>This is a ClojureScript Function!</Message>
</Response>

Let’s move on to some more interesting messages.

Adding the Random Hickey Quotes

I have created a quotes namespace on my GitHub project which contains a few of my favourite Hickey quotes from Azel4321’s Clojure Quotes repo. You can copy mine straight from GitHub or write your own in src/main/quotes.cljs, as you prefer.

Requiring the quotes namespace and using rand-nth to select one, the function now looks like this:

(ns my-twilio-function
  (:require-macros [hiccups.core :as hiccups :refer [html]])
  (:require [hiccups.runtime :as hiccupsrt]
                [quotes :as q]))

(defn my-function [context event callback] 
  (callback nil
                (doto (new js/Twilio.Response)
                  (.appendHeader "content-type" "application/xml")
                  (.setBody (html [:Response
                                   [:Message
                                    (rand-nth q/hickey-quotes)]])))))

 

Protecting our Function

To prevent Twilio Functions from being accessible outside of the context of a Twilio webhook call, we can require that the requests be signed. To enable this we only need to change the name of the generated JavaScript file to end .protected.js instead of just .js. In shadow-cljs.edn:

   :output-to "functions/my-function.protected.js"

The function can still be accessed as before when running on localhost but when deployed the platform will check for a valid signature before executing your code.

Building and Deploying

Again, build with npx shadow-cljs release function. The path for the function will still be /my-function so delete functions/my-function.js to prevent the error “Duplicate. Path /my-function already exists”.

Deploy with twilio serverless:deploy again, and the existing function will be replaced with this new one. Because this function now requires the webhook signature, attempting to load the public URL in a browser will result in an “HTTP 403 Forbidden” error.

To buy a phone number and configure Twilio to use this function to respond to SMS you can use the Twilio CLI:

twilio phone-numbers:buy:mobile --country-code GB # replace with your country code
twilio phone-numbers:update <the phone number you just bought> \
         --sms-url <your public function URL>

Now you can hear from Rich Hickey whenever you like:

Screenshot of my phone showing a text message conversation with "(not) Rich Hickey". He encourages us all to embrace the fact that Clojure is different.

What Next?

 

I can’t wait to see what you build. Let me know by email mgilliard@twilio.com or on Twitter @MaximumGilliard