Make phone calls with Crystal and Twilio

Phil Nash - Jul 3 '17 - - Dev Community

We've already learned how to send SMS messages with Crystal, a new language that looks like Ruby but runs like C. Doing that required making a simple HTTP request from our Crystal application to the Twilio API.

Now it's time to dig a little deeper and see how to make phone calls with Crystal. Making a call is going to require not only an HTTP request but also receiving a webhook which we will need to respond to with TwiML.

Getting started

If you want to join in, you'll need to install Crystal, there are installation instructions in the documentation. You will also need a Twilio account, which you can get for free if you don't have one yet, and a Twilio number that is capable of making phone calls.

We'll use the code from the previous post as the starting point for this project. You can run through that post and learn how to send a text message with Crystal then come back here to make a call, or just grab that post's completed code from GitHub.

Making a phone call with Crystal

Let's start by creating a new Crystal file to write the code that will make phone calls.

$ touch phone.cr
Enter fullscreen mode Exit fullscreen mode

We'll use the code we wrote in the sms.cr file as the starting point for making phone calls so open that file and copy and paste the code from it into phone.cr.

Now we can make a few modifications so we're making calls instead of sending messages.

First, make a simple JSON mapping for our call object.

Next, rename the function from send_sms to make_calls and change the last parameter from body to url. The url will need to point to an application that can handle incoming HTTP requests. When the call connects, Twilio will make a request, known as a webhook, to the url to find out what to do next. We'll see how that is implemented later.

Change the post_form URL so that we're calling the Calls resource.

Finally parse the response from Twilio into a Call object. Here's the completed code:

require "http/client"
require "json"

class Call
  JSON.mapping(
    sid: String
  )
end

class Error
  JSON.mapping(
    message: String,
    status: Int32
  )
end

def make_call(to, from, url)
  client = HTTP::Client.new("api.twilio.com", 443, true) do |client|
    client.basic_auth(ENV["TWILIO_ACCOUNT_SID"], ENV["TWILIO_AUTH_TOKEN"])
    response = client.post_form("/2010-04-01/Accounts/#{ENV["TWILIO_ACCOUNT_SID"]}/Calls.json", {
      "To"   => to,
      "From" => from,
      "Url" => url,
    })
    if response.success?
      call = Call.from_json(response.body)
      puts call.sid
    else
      error = Error.from_json(response.body)
      puts error.status, error.message
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

Add one final line to this file that calls make_call with your own phone number, your Twilio number and the URL set to "https://dl.dropboxusercontent.com/u/2554/crystal-call.xml".

make_call(YOUR_NUMBER, YOUR_TWILIO_NUMBER, "https://dl.dropboxusercontent.com/u/2554/crystal-call.xml")
Enter fullscreen mode Exit fullscreen mode

Save the file, compile and run it.

$ crystal phone.cr
Enter fullscreen mode Exit fullscreen mode

You should receive a phone call which then tells you what to do next.

Hint: it tells you to read on.

We've made a call and responded to the incoming webhook using static TwiML. To make more interactive applications it would be better to create dynamic TwiML in Crystal. Let's investigate how to do that, first with Crystal's HTTP::Server class.

Building HTTP Servers in Crystal

Create a new Crystal file for our server.

$ touch server.cr
Enter fullscreen mode Exit fullscreen mode

Open that file and start by requiring the http/server module from the standard library.

require "http/server"
Enter fullscreen mode Exit fullscreen mode

The HTTP::Server is quite straightforward to get started with. Create an instance of HTTP::Server passing the port you want it to listen on. We also pass a block that will handle incoming requests.

require "http/server"

server = HTTP::Server.new(3000) do |context|

end
Enter fullscreen mode Exit fullscreen mode

The context is an HTTP::Server::Context that is passed to our handler when the server receives a request. It has references to request and response objects. We can use the response to set the content type to text/xml as we'll be returning TwiML.

require "http/server"

server = HTTP::Server.new(3000) do |context|
  context.response.content_type = "text/xml" 
end
Enter fullscreen mode Exit fullscreen mode

Then, for the purposes of this post, we can print our TwiML straight to the response too. To show that this is creating the TwiML dynamically we'll print a message that will change over time.

require "http/server"

server = HTTP::Server.new(3000) do |context|
  context.response.content_type = "text/xml" 
  time = Time.now
  message = if time.hour < 12
              "Good morning"
            elsif time.hour < 18
              "Good afternoon"
            else
              "Good night"
            end
  context.response.print "<Response><Say>#{message} from Crystal!</Say></Response>"
end
Enter fullscreen mode Exit fullscreen mode

To finish up with this very simple server we need to tell the server to start listening for incoming connections. We also print a message to the terminal so that we can see the server has started.

require "http/server"

server = HTTP::Server.new(3000) do |context|
  context.response.content_type = "text/xml"
  time = Time.now
  message = if time.hour < 12
              "Good morning"
            elsif time.hour < 18
              "Good afternoon"
            else
              "Good night"
            end
  context.response.print "<Response><Say>#{message} from Crystal!</Say></Response>"
end

puts "Listening on http://0.0.0.0:3000"
server.listen
Enter fullscreen mode Exit fullscreen mode

Compile and run the server:

$ crystal server.cr
Enter fullscreen mode Exit fullscreen mode

Open your browser to http://localhost:3000 and you'll see your TwiML. We can now use this server to drive our call. To do so, we need to open up our development server to external requests. I like to do this with ngrok, you can find out how to install and use it here. If you have ngrok setup open up a tunnel to port 3000.

$ ngrok http 3000
Enter fullscreen mode Exit fullscreen mode

Copy your ngrok URL and open up phone.cr again. Replace the DropBox link in the call to make_call with your ngrok URL and compile and run phone.cr again.

$ crystal phone.cr
Enter fullscreen mode Exit fullscreen mode

This time when your phone rings you'll hear the message you wrote in server.cr.

Better servers

We've built a very primitive server so far using the standard library. We could do the work to build in support for dynamic paths, body parsing, HTTP verbs, etc, but as in any language, it's good to see what the community has provided. Crystal comes with an integrated package manager and there are already a number of packages, known as shards, available.

To improve our server offering, we're going to replace our basic HTTP server with Kemal, the Crystal equivalent of Ruby's Sinatra.

This time, we're going to generate our project with Crystal's init tool. You can initialise an app or a library, we want an app for now.

$ crystal init app phone_server
$ cd phone_server
$ ls
LICENSE        README.md         shard.yml        spec        src
Enter fullscreen mode Exit fullscreen mode

init gives us a basic project structure including a shard.yml file. Open that up and add the dependency on Kemal.

name: phone_server
version: 0.1.0

license: MIT

dependencies:
  kemal:
    github: kemalcr/kemal
    branch: master
Enter fullscreen mode Exit fullscreen mode

On the command line install the dependencies:

$ shards install
Enter fullscreen mode Exit fullscreen mode

Open up src/phone_server.cr. and replace everything in the file with:

require "kemal"
Enter fullscreen mode Exit fullscreen mode

By default Twilio makes webhooks via POST request. In Kemal we define routes by the HTTP verb that we want to handle, our simple HTTP::Server responded to everything. To handle the webhook create an endpoint for a POST request at "/voice". Handlers in Kemal get a similar context object to the HTTP::Server.

require "kemal"

post "/voice" do |context|

end
Enter fullscreen mode Exit fullscreen mode

We can use the response property of the context to set headers.

require "kemal"

post "/voice" do |context|
  context.response.content_type = "text/xml"
end
Enter fullscreen mode Exit fullscreen mode

Then we just need to return our TwiML as we did in the plain HTTP::Server example.

require "kemal"

post "/voice" do |context|
  context.response.content_type = "text/xml"
  time = Time.now
  message = if time.hour < 12
              "Good morning"
            elsif time.hour < 18
              "Good afternoon"
            else
              "Good night"
            end
  "<?xml version="1.0" encoding="UTF-8"?>
  <Response>
    <Say>#{message} from Kemal and Crystal!</Say>
  </Response>"
end
Enter fullscreen mode Exit fullscreen mode

And finally run Kemal.

require "kemal"

post "/voice" do |context|
  context.response.content_type = "text/xml"
  time = Time.now
  message = if time.hour < 12
              "Good morning"
            elsif time.hour < 18
              "Good afternoon"
            else
              "Good night"
            end
  "<?xml version="1.0" encoding="UTF-8"?>
  <Response>
    <Say>#{message} from Kemal and Crystal!</Say>
  </Response>"
end

Kemal.run
Enter fullscreen mode Exit fullscreen mode

If you haven't already, stop your old server with Ctrl C. Now compile and run the new, Kemal based server:

$ crystal src/phone_server.cr
[development] Kemal is ready to lead at http://0.0.0.0:3000
Enter fullscreen mode Exit fullscreen mode

Hopefully you still have ngrok running, otherwise start it again and copy the ngrok URL to your make_call function in phone.cr. Make sure to add the "/voice" path this time. Your URL should look like, "http://RANDOM.ngrok.io/voice".

Compile and run phone.cr once more. This time when you answer your ringing phone you will hear the message you wrote in your Kemal server.

I told you it was fast

I mentioned in my first post on Crystal that it is blazingly fast. Before you kill your Kemal server I want you to take a note of the response time it reported. Here's what I saw:

The server logs show that the request was served in 54 micro seconds.

Yes, that says 54 micro seconds. I realise this endpoint is very simple, but that sort of raw performance from a framework does impress me. That's not all though. This was compiled in debug mode. Compile the application for release with:

$ crystal build src/phone_server.cr &mdash;release
Enter fullscreen mode Exit fullscreen mode

And then run the executable and things can get even quicker.

A few runs of the server compiled in release mode show response times of between 22 and 48 micro seconds.

Take Crystal for a spin

So there we have it, we've sent SMS messages and made phone calls with Crystal. We've seen a bit on how to use the HTTP::Client, JSON mappings, the HTTP::Server, shards and Kemal. You can check out all the code we wrote in both blog posts in this GitHub repo.

If you want to explore Crystal more then take a look through the documentation, the standard library, Kemal's documentation, the list of available CrystalShards, or even the source code for Crystal (it's all written in Crystal!).

Like the look of Crystal? Let me know in the comments below or drop me an email or a note on Twitter at @philnash.


Make phone calls with Crystal and Twilio was originally published on the Twilio blog on December 2, 2016.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .