Sharing music over playlists is a great way to discover new and old music. Spotify has collaborative playlists, but I don't like how they let your friends re-order and delete songs from the list. We can fix this by building our own collaborative playlist that only allows additions using the Spotify Web API. With the Twilio API for WhatsApp we can let our friends send in a song whenever they are struck by inspiration.
In this post we are going to build a WhatsApp bot that can do all of the above using Ruby on Rails.
Getting started
To build this application we will need a few things:
- Ruby version 2.5.1 installed
- Rails and bundler, which can be installed with
gem install rails bundler
- A Twilio account (sign up for a free Twilio account here)
- The WhatsApp sandbox set up in our Twilio account
- A Spotify developer account
- ngrok, so we can tunnel through to our locally developed application to use webhooks
Spotify API Credentials
To work with the Spotify API we will need to generate some API credentials. From the Spotify developer dashboard create a new application and follow the 3 step process.
- First, fill in some details about the app. Name it, I called mine WhatsPlaying, provide a short description and check what app type it is, I chose "website"
- Next we are asked if this is a commercial integration; choose "no"
- Finally, check the agreements
Once these steps are complete we will see the application dashboard. We will need the Client ID and Client Secret from this page.
There's one more thing we need to do here. Click on "edit settings", enter the redirect URL http://localhost:3000/auth/spotify/callback
and click "add".
Now that credentials are sorted, we can start to build our app.
Preparing the project
I've started the project off, so clone or download the repo from GitHub and checkout the getting-started
branch.
git clone -b getting-started https://github.com/philnash/whats_playing.git
Change into the directory and install the project's dependencies.
cd whats_playing
bundle install
Copy the config/env.yml.example
file to config/env.yml
(we're using envyable to manage environment variables for the application). Open config/env.yml
and fill in the Spotify Client ID and Client Secret we just generated. We'll fill in the rest soon.
We're now going to generate a Spotify access token for your account on Spotify as well as a new playlist, called "WhatsPlaying", that we are going to use as our collaborative playlist. Run the Rails app with:
bundle exec rails server
Open the app at http://localhost:3000. There will be a link to "Sign in with Spotify". Click the link and follow the OAuth process. All of this is handled by the RSpotify gem and omniauth, you can check out the config required to make this work in config/initializers/rspotify.rb
.
Once you have been directed back to the application you will see your Spotify user ID, access token, refresh token, and playlist ID on the page in front of you. Copy these into config/env.yml
, restart the application, and open http://localhost:3000 again. You will find the playlist that the script just created empty and embedded in the page. Now it's time to build the integration between WhatsApp, Twilio, and Spotify.
Getting to grip with webhooks
When you send a message to a WhatsApp number that you control with Twilio, Twilio will send an HTTP request, or webhook, to an application URL we provide. We will use this to respond to incoming messages and add songs to our playlist. Let's set up our webhook endpoint first.
To do so we will install the twilio-ruby gem. This will make writing TwiML, the XML that Twilio understands and takes direction from, easier. Open up the Gemfile
and under the line that includes RSpotify, add twilio-ruby:
gem 'rspotify', '~> 2.1.1'
gem 'twilio-ruby', '~> 5.12.3'
Install the gem by running bundle install
in the project directory again.
We now need a new controller to handle the incoming webhooks. Use the Rails generator to create a new one:
bundle exec rails generate controller twilio/messages
Open up the newly generated app/controllers/twilio/messages_controller.rb
and start by removing the default Rails CSRF protection. Webhooks are, by definition, a cross site request and we should secure them by validating that the request came from Twilio. We can do so later using the Twilio request validation Rack middleware provided by the twilio-ruby gem.
class Twilio::MessagesController < ApplicationController
skip_before_action :verify_authenticity_token
end
Next we need an action to handle the incoming webhook requests. In the context of Rails, I consider webhooks to be create
actions, so make a new create
method.
class Twilio::MessagesController < ApplicationController
skip_before_action :verify_authenticity_token
def create
end
end
To make sure we're on track, let's make this respond with a simple message first. We'll use the TwiML generation capability of the twilio-ruby gem to respond to any incoming message with the customary "Hello World!".
class Twilio::MessagesController < ApplicationController
skip_before_action :verify_authenticity_token
def create
response = Twilio::TwiML::MessagingResponse.new
response.message(body: "Hello World!")
render xml: response.to_xml
end
end
This action needs a route too. Open config/routes.rb
and add the following:
Rails.application.routes.draw do
namespace :twilio do
post 'messages', to: 'messages#create'
end
# other routes
end
This is all we need to get a response, so let's hook up the Twilio WhatsApp sandbox to this application.
Start your app:
bundle exec rails server
Start up an ngrok tunnel so that the Twilio webhook can reach your app with a public URL. In a new terminal window on the command line run:
ngrok http 3000
Take the URL ngrok generates, add the /twilio/messages
path and enter it as the URL when "A MESSAGE COMES IN" for the WhatsApp channel.
Send a message to the WhatsApp sandbox number and you will get a response back saying "Hello World!". Now it's time to hook up the Spotify API.
Connecting with the Spotify API
We're going to build a conversational interface to interact with the Spotify API. We'll assume that when a user sends their first message it is a song title they want to add to the playlist. We'll use the Spotify API to search for that song and send the user back the first result to see if that's what they wanted. If they confirm with a positive response then we'll add the track to the playlist. If they say no we'll ask them to search again. If they send anything else back we can assume that is a new search.
We're using the excellent RSpotify library for this application, but let's create a class that is more suited to our use. Create a new directory in the app
directory called services
and a file in there called spotify.rb
. Open spotify.rb
and create a new class:
class Spotify
end
In the initializer we will use the credentials we created earlier to create an authenticated user.
class Spotify
def initialize(user_id, user_token, user_refresh_token)
@user = RSpotify::User.new({
'id' => user_id,
'credentials' => {
'token' => user_token,
'refresh_token' => user_refresh_token
}
})
end
end
Searching for a track is performed by passing a query to the RSpotify::Track.search
method and we only want to return the first result.
class Spotify
def initialize(user_id, user_token, user_refresh_token)
end
def track_search(query)
RSpotify::Track.search(query).first
end
end
We can add tracks to a playlist by getting the track ID from the result of track_search
, searching for the playlist by ID, and then adding the track to the playlist.
class Spotify
def initialize(user_id, user_token, user_refresh_token)
end
def track_search(query)
end
def add_to_playlist(playlist_id, track_id)
playlist = RSpotify::Playlist.find(@user.id, playlist_id)
playlist.add_tracks!([track_id])
end
end
Let's use this class in our controller now, open app/controllers/twilio/messages_controller.rb
again. At the start of the create
method, get the body of the message, which we'll use as the search term and instantiate a new Spotify object using the credentials in the environment.
Searching for songs
We'll start by testing the search functionality and returning the name and artist of the first track we find. Spotify returns the artists as a list as there could be more than one responsible for a track. We can turn that into a sentence with a bit of ActiveSupport trickery. WhatsApp also supports some basic message formatting; if we surround our track and artist names with underscores, they will appear in italics.
class Twilio::MessagesController < ApplicationController
skip_before_action :verify_authenticity_token
def create
body = params['Body']
spotify = Spotify.new(ENV['SPOTIFY_USER_ID'], ENV['SPOTIFY_TOKEN'], ENV['SPOTIFY_REFRESH_TOKEN'])
track = spotify.track_search(body)
message = "Did you want to add _#{track.name}_ by _#{track.artists.map(&:name).to_sentence}_?"
response = Twilio::TwiML::MessagingResponse.new
response.message(body: message)
render xml: response.to_xml
end
end
Restart the application and send a message, searching for a track you want to hear. You should get a message back listing the track you wanted. If you get a different song, try adding the artist or album to narrow down the search.
We do need to to handle the case if we don't find any songs though.
def create
body = params['Body']
spotify = Spotify.new(ENV['SPOTIFY_USER_ID'], ENV['SPOTIFY_TOKEN'], ENV['SPOTIFY_REFRESH_TOKEN'])
track = spotify.track_search(query)
if track
message = "Did you want to add _#{track.name}_ by _#{track.artists.map(&:name).to_sentence}_?"
else
message = "I couldn't find any songs by searching for '#{body}'. Try something else."
end
response = Twilio::TwiML::MessagingResponse.new
response.message(body: message)
render xml: response.to_xml
end
Adding to the playlist
We can handle search requests. Now we need to be able to add the track if the user responds with a positive answer.
Like SMS conversations on Twilio, we can use the session to retain data between messages. For this application we can store the track URI, which identifies it to Spotify, in the session. Then, if we have a track in the session we can look for a "yes" in the response. If we get "yes", we add the track to the playlist and clear the session.
def create
body = params['Body']
spotify = Spotify.new(ENV['SPOTIFY_USER_ID'], ENV['SPOTIFY_TOKEN'], ENV['SPOTIFY_REFRESH_TOKEN'])
if session[:track]
answer = body.split(' ').first.downcase.strip
if ['yes', 'yeah', 'yep', 'yup', '👍'].include? answer
message = "OK, adding that track now."
spotify.add_to_playlist(ENV['SPOTIFY_PLAYLIST_ID'], session[:track])
session[:track] = nil
end
end
if !message
track = spotify.track_search(body)
if track
session[:track] = track.uri
message = "Did you want to add _#{track.name}_ by _#{track.artists.map(&:name).to_sentence}_?"
else
message = "I couldn't find any songs by searching for '#{body}'. Try something else."
end
end
response = Twilio::TwiML::MessagingResponse.new
response.message(body: message)
render xml: response.to_xml
end
I added some other options for saying "yes" including the emoji thumbs up 👍. Who doesn't love emoji?
You can test this path through the application by sending a song to the WhatsApp number and following up with "yes". Then check that the song appears in the playlist.
Other responses
To complete our Spotify WhatsApp bot we need to handle the other potential responses. If the user sends back a negative response, then we want to ask what track they want to add instead. Any other response and we'll assume it's a new search.
def create
body = params['Body']
spotify = Spotify.new(ENV['SPOTIFY_USER_ID'], ENV['SPOTIFY_TOKEN'], ENV['SPOTIFY_REFRESH_TOKEN'])
if session[:track]
answer = body.split(' ').first.downcase.strip
if ['yes', 'yeah', 'yep', 'yup', '👍'].include? answer
message = "OK, adding that track now."
spotify.add_to_playlist(ENV['SPOTIFY_PLAYLIST_ID'], session[:track])
session[:track] = nil
elsif ['no', 'nah', 'nope', '👎'].include? answer
session[:track] = nil
message = "What do you want to add?"
end
end
if !message
track = spotify.track_search(body)
if track
session[:track] = track.uri
message = "Did you want to add _#{track.name}_ by _#{track.artists.map(&:name).to_sentence}_?"
else
message = "I couldn't find any songs by searching for '#{body}'. Try something else."
end
end
response = Twilio::TwiML::MessagingResponse.new
response.message(body: message)
render xml: response.to_xml
end
Restart the app one more time and start searching and confirming or denying tracks. You can see the songs you add appear on your playlist and in the embedded playlist on the homepage of your app too.
A bot and a brand new playlist
With the Spotify API and the Twilio API for WhatsApp we've just built a collaborative playlist. The interaction is quite simple right now, but you could build on this to include voting on songs to move them up in the playlist or ways to better share the playlist with more friends.
If you want to see all the code for this application, check out the repo on GitHub.
I thought it would be fun to see what you would add to the playlist, so I've deployed my version of this app. You can add songs to it by connecting to my WhatsApp sandbox by messaging " join beaver-wrasse" to this Twilio WhatsApp number: +14155238886. Then start sending me all your favourite songs! Check out the playlist online here!
Got any ideas for collaborative applications you can build with WhatsApp and Twilio? Let me know in the comments, on Twitter or by email at philnash@twilio.com.