Build a location-aware WhatsApp weather bot with Ruby, Sinatra and Twilio

Phil Nash - Mar 20 '20 - - Dev Community

We've seen how to write a text chatbot using the Twilio API for WhatsApp using Ruby, but WhatsApp also supports sending and receiving location data via their API. In this post we are going to see how to build a WhatsApp bot that can receive and respond to location messages.

We'll build a weather bot so that you can send your location to the bot to get your local weather forecast.

What you'll need

To code along with this post and build your own location-aware WhatsApp bot you will need:

Configure your WhatsApp sandbox

If you want to launch a bot on WhatsApp you need to get approval from WhatsApp, but Twilio allows you to build and test your WhatsApp bots using our sandbox. Let's start by configuring the sandbox to use with your WhatsApp account.

If you haven't set up your WhatsApp sandbox, head to the Twilio console WhatsApp Sandbox and follow the instructions in my previous post. When you've received a message back over WhatsApp you're ready to continue.

The Twilio Sandbox for WhatsApp first screen. It tells you to send a message to the WhatsApp number +1 415 523 8886 with a code that starts with "join".

Setting up the Ruby application

At the end of the last post, we had a good base application which we can start with this time. Get this setup by cloning it from GitHub and changing into the directory:

git clone https://github.com/philnash/ruby-whatsapp-bots.git
cd ruby-whatsapp-bots
Enter fullscreen mode Exit fullscreen mode

Install the dependencies:

bundle install
Enter fullscreen mode Exit fullscreen mode

Copy config/env.yml.example to config/env.yml and fill in your Twilio Auth Token (available in your Twilio console).

Copy the base directory to one called location_bot.

cp -R base location_bot
Enter fullscreen mode Exit fullscreen mode

Open location_bot/config.ru and change the last line to:

run LocationBot
Enter fullscreen mode Exit fullscreen mode

Open location_bot/bot.rb. Change the class name to LocationBot and check out the current contents:

require "sinatra/base"

class LocationBot < Sinatra::Base
  use Rack::TwilioWebhookAuthentication, ENV['TWILIO_AUTH_TOKEN'], '/bot'

  post '/bot' do
    response = Twilio::TwiML::MessagingResponse.new
    response.message body: "This is the base bot. Edit me to make your own bot."
    content_type "text/xml"
    response.to_xml
  end
end
Enter fullscreen mode Exit fullscreen mode

This sets up an endpoint at /bot to receive incoming webhooks from Twilio and respond with a message in a TwiML response. It also protects the endpoint from outsiders by vallidating the signature. Run the application with:

bundle exec rackup location_bot/config.ru
Enter fullscreen mode Exit fullscreen mode

and once your application server boots up then everything is ready to go.

Receiving location messages in WhatsApp

Whenever a user sends a message to your WhatsApp number Twilio will turn that into a webhook to the endpoint you set up in the console. The webhook is an HTTP request with all the details about the incoming message. Previously we worked with the message Body but this time we want to deal with location messages.

WhatsApp allows a user to send text or location, but not both at the same time. When a user sends a location the incoming webhook will include Latitude and Longitude parameters. You might also receive Label and Address parameters if the user chooses a named location to share. We can access all of these bits of data in Sinatra via the params hash.

We can take a look at this data by logging it out. Add the following to location_bot/bot.rb:

  post '/bot' do
    puts "Latitude: #{params["Latitude"]}"
    puts "Longitude: #{params["Longitude"]}"
    puts "Label: #{params["Label"]}"
    puts "Address: #{params["Address"]}"
    response = Twilio::TwiML::MessagingResponse.new
    response.message body: "This is the base bot. Edit me to make your own bot."
    content_type "text/xml"
    response.to_xml
  end
Enter fullscreen mode Exit fullscreen mode

Restart the application or run it with:

bundle exec rackup location_bot/config.ru
Enter fullscreen mode Exit fullscreen mode

To test this we'll need to open up a tunnel to our server running on our machine. I recommend using ngrok for this. If you don't have it installed follow the instructions on ngrok.com and when you're ready, run:

ngrok http 9292
Enter fullscreen mode Exit fullscreen mode

This will open a tunnel pointed to localhost port 9292 which is where Sinatra is hosted by default using rackup. You will find a public ngrok URL that now points at your local application. Open up the WhatsApp Sandbox in your Twilio console and enter that URL plus the path /bot into the field labelled When a message comes in.

The Twilio Sandbox for WhatsApp config page. You should add your ngrok URL into the field labelled "When a message comes in".

Send the Sandbox number a location message, by hitting the plus button and then choosing the location to share.

In the WhatsApp application, press the plus button next to the text input and then choose location from the menu.

In the terminal you will see the location information printed out.

A terminal window showing the logs from the request. I sent a message from the Twilio office and it shows the latitude, longitude, label and address.

Let's take this location data and use it to return a localised weather report. For this blog post, we'll use the Dark Sky API. It's a simple API and you can sign up for a free API key here. Once you have your API key, open config/env.yml and add the Dark Sky API key under your Twilio auth token.

TWILIO_AUTH_TOKEN: YOUR_TWILIO_AUTH_TOKEN
DARK_SKY_API_KEY: YOUR_DARK_SKY_API_KEY
Enter fullscreen mode Exit fullscreen mode

Let's write a small class that can make HTTP requests to the API to find the weather for the latitude and longitude that we are getting from the WhatsApp location message.

Calling the Dark Sky API

The Dark Sky API URL format looks like this:

https://api.darksky.net/forecast/{API_KEY}/{LATITUDE},{LONGITUDE}?{OPTIONS}
Enter fullscreen mode Exit fullscreen mode

In the last blog post we used the http.rb library to make requests to various dog and cat APIs. We can use that again to make requests to the Dark Sky API. Let's build a small class to do this. At the bottom of location_bot/bot.rb add the following:

class DarkSky
  def initialize(api_key)
    @api_key = api_key
  end

  BASE_URL = "https://api.darksky.net/forecast/"

  def forecast(lat, long)

  end
end
Enter fullscreen mode Exit fullscreen mode

This is a good starting point that gives us a class we can initialize with our API key. It has stored the base URL for the API and we have a forecast method that will take the latitude and longitude. Complete the forecast method with the following:

  def forecast(lat, long)
    url_options = URI.encode_www_form({ :exclude => "minutely,daily,alerts,flags", :units => "si" })
    url = "#{BASE_URL}#{@api_key}/#{lat},#{long}?#{url_options}"
    response = HTTP.get(url)
    result = JSON.parse(response.to_s)
  end
Enter fullscreen mode Exit fullscreen mode

The first line of the method includes a few request parameters. In this case we are requesting SI (metric) units for the responses and we are excluding some of the detail of the response. We then build up the URL encoding the query parameters using the standard library's URI#encode_www_form method. We pass the URL to the HTTP.get method to get a response and then parse that response body as JSON, returning the result.

We can now use this class in our bot.

Sending a weather forecast based on WhatsApp location

Return to the code that responds to the incoming WhatsApp message. Now we want to check if the incoming message is a location message by checking for the Latitude and Longitude parameters. If it is we'll make the call to the Dark Sky API using the class we just wrote but if it isn't we'll return a message asking for the user's location.

Start by removing the debugging puts calls and then build up the conditional.

  post '/bot' do
    response = Twilio::TwiML::MessagingResponse.new

    if params["Latitude"] && params["Longitude"]
      dark_sky = DarkSky.new(ENV['DARK_SKY_API_KEY'])
      forecast = dark_sky.forecast(params["Latitude"], params["Longitude"])
      forecast_message = "It is currently #{forecast["currently"]["summary"].downcase} with a temperature of #{forecast["currently"]["temperature"].to_s.split(".").first}°C.\nForecast: #{forecast["hourly"]["summary"].downcase}"
      response.message body: forecast_message
    else
      response.message body: "To get a weather forecast, send your location from WhatsApp."
    end
    content_type "text/xml"
    response.to_xml
  end
Enter fullscreen mode Exit fullscreen mode

Above we use the forecast returned by the Dark Sky API to build up a message made up of the current weather summary and temperature followed by the hourly forecast summary. This looks like:

I sent a location message from the Twilio Melbourne office and received the following reply: "It is currently clear with a temperature of 25°C. Forecast: partly cloudy throughout the day."

Success! We received a WhatsApp location message and turned that into a weather forecast for that location.

Power up your bots with location

Location can be hugely useful with bots and this is a feature that the Twilio API for WhatsApp has over other channels like SMS. If you want to see the entire code for this bot and others, check out the GitHub repo here. You can find the location bot under the location directory.

There are loads of other things you could do with a location-aware bot. You could find nearby stores, send directions, or provide local insights for your users. I'd love to hear any ideas you have for location based bots too. Let me know in the comments or on Twitter at @philnash.

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