Build your first Facebook Messenger bot in Ruby with Sinatra (Part 2/3)

Written by progapanda | Published 2016/12/21
Tech Story Tags: bots | facebook-messenger | ruby | programming | learning-to-code

TLDRvia the TL;DR App

Keeping up with the conversation

A less obscure cultural reference

In the Part 1 of this tutorial we set up our page on Facebook and hooked it to a Sinatra server running on our machine through ngrok. Then we introduced the facebook-messenger gem by the guys at Hyperoslo. We also taught our bot to use Google Geocoding API in order to lookup GPS coordinates for any given address. This is what our bot will look like after we meddle with it some more:

Chatting with the bot onĀ mobile

Talk toĀ me

OK, letā€™s get first things about Messenger bots straight. They adhere to Victorian etiquette for little kids:

ā€œDo not speak unless spokenĀ toā€

This is a good rule, because it helps to avoid unsolicited messages aka spam. But how do we provide solicited messages? In the first minimalistic version of our bot we just assumed that the user will give us something we can send directly to Google Geocoding API, wait for response and display the result. That assumes userā€™s prior knowledge about our botā€™s intent, which is a very bad way to go about UX. Also, we did not provide any fallbacks in case user types anything that doesnā€™t have GPS coordinates (surprisingly, ā€˜Helloā€™ will yield a result, as Google will just assume weā€™re looking for Hella, Iceland)ā€Šā€”ā€Šin that case, our bot will completely lose its marbles. We need some kind of conversational loop.

Poorly Drawn Lines by Reza Farazmand is licensed under a CC BY-NCĀ 3.0

First thing I learned while trying to implement it with facebook-messenger gem: Bot.onĀ :message { |ā€¦| block } call can be made only once per scope, otherwise we face all sorts of unexpected behaviour (correct me in replies if Iā€™m wrong). So we need to create more scopes! Fine, letā€™s define separate methods to handle different ways our conversation can go, starting with the most obvious one. Inside bot.rb delete your Bot.on method that takes a block and replace it with this:

Now we need to add a process_coordinates method referenced in our wait_for_user_input. Put this into your bot.rb:

Now at the bottom of bot.rb call our waiting method. It will launch the conversation loop:

wait_for_user_input

From now on we will only engage user if he mentions ā€œcoordinatesā€ (ā€œcoordā€ as a shortcut) or ā€œgpsā€ in any context. Then we will ask user to provide a destination for look-up, then we will call Google and display the result. If no result is found, we will notify user and wait for another command. At this stage, our code.rb looks like this. Hit Ctrl-C in your tab with rackup running and restart the server to test it in Messenger.

OK, it seems to work. Letā€™s add features!

If we dig inside a JSON we get back from Google, weā€™ll see that thereā€™s a key called formatted_address that will return a full postal address for any destination Google can find. Letā€™s add this as a feature to our bot.

First, add another when clause to our wait_for_user_input method:

Note that that we thought of user misspelling ā€œaddressā€: if he/she types ā€œI need a full adressā€ (a type of mistake Iā€™d make) our regexp will still match and the show_full_address function will be called. Put it inside bot.rb too:

Finally, add this helper method:

Great. This is your code at this point. Restart the server and test.

Time to refactor!

Our bot surely works and can handle two different commands, but its code could certainly use some refactoring: process_coordinates and show_full_address methods are 80% identical, that means we can do better.

Refactoring can sure getĀ messy

We know that Ruby methods can accept blocks. It seems we may benefit from this language feature to make our code DRY-er. Letā€™s define another method that will abstract out most of API handling functionality:

Then we can change both process_coordinates and show_full_address like so:

Great. Now our methods only do stuff that is actually specific to their purpose, the rest is handled by our generic method that handles API-related logic.

I know, the concept of Ruby yield with arguments can be tricky to wrap your head around, especially if you are a beginner coder like me, but I like to think of it as time-space warp that connects two methods, and you can pass variables back and forth.

This is how I imagine Ruby yield that takes an argument inside a methodĀ (Almost)

While we are at it, letā€™s also create an IDIOMS constant, so we can have all our botā€™s phrases in one place. Add it on top of your bot.rb:

IDIOMS = {not_found: 'There were no resutls. Ask me again, please',ask_location: 'Enter destination'}

Now replace your hard coded replies with a reference to IDIOMS like so:

message.reply(text: IDIOMS[:ask_location])

This is your code at this stage. Restart your server and test test test.

Not justĀ talk

Great, out bot still works and we used some clever Ruby, but it is still not the best user experience. How the user is supposed to know what to do? Can we guide him? The promise of conversational AI did not really fulfil itself yet, and for now we are stuck with mixed interfaces. Everyone is doing it. Facebook Messenger has currently three (that I know of) ways to show ā€œmenusā€ to the user:

  1. A persistent menu that can always be called from the icon next to text entry field.
  2. A ā€œButtonā€ template that letā€™s you tie action buttons to a message bubble.
  3. A set of ā€œquick repliesā€.

Three Facebook-approved ways of presenting button menus toĀ user

We will pick quick replies as I find them more user-friendly. They are also treated as regular messages by the API, while other buttons implement ā€œpostbackā€ call to your webhook, which is a bit different concept, as we will see later.

First, create constant to keep your quick replies in one place:

ā€œPayloadā€ sounds confusing, but this is really just another identifier you can assign to user interaction. Think of it as a constant, so we use CAPITAL_LETTERS for our payloads.

Okay, now we are going to write a little method that will help us a lot in the future to adopt more declarative approach in the way we code botā€™s messages. Remember I told you bots only speak when they are spoken to? Well, thatā€™s true up to a point. A user must do something that will expose his ID. Like, sending first message or using a link to your Facebookā€™s page address (where your bot lives and handles its Messenger interaction).

So, in addition to Bot.onĀ :message { |ā€¦| block } that we saw earlier, facebook-messenger gem creators also gave us Bot.deliver method that lets us send messages to user out of the blue once we have his Facebook ID. We are going to wrap it into our own say helper:

It will take a userā€™s id, a text for the message we want to send and optional argument for an array of quick replies. If an argument is supplied, message will be sent with quick replies attached to it, if notā€Šā€”ā€Šit will be just a text.

From this great article in Chatbotā€™s Life we know that most users strike a conversation with a bot with some form of ā€˜hiā€™ or ā€˜helloā€™. We want our bot to present whatā€™s on the menu ASAP, without any unnecessary meet & greet, so we will cheat a little: weā€™ll introduce the wait_for_any_input method that will present our array of quick replies to whatever user sends us, be it ā€˜hiā€™, ā€˜bonjourā€™ or ā€˜ŠæрŠøŠ²ŠµŃ‚ā€™. Add it to bot.rb:

message.sender[ā€˜idā€™] is how we get userā€™s ID after he opened the conversation.

Also, letā€™s change the name of our old wait_for_user_input method to wait_for_command to avoid any ambiguity. Make sure to change the definition and all possible calls.

Also, make sure that at the very end of our bot.rb you now call wait_for_any_input:

# launch the loopwait_for_any_input

Letā€™s update our IDIOMS:

IDIOMS = {not_found: 'There were no resutls. Ask me again, please',ask_location: 'Enter destination',unknown_command: 'Sorry, I did not recognize your command',menu_greeting: 'What do you want to look up?'}

Then letā€™s define our show_replies_menu method:

It uses our say helper to send user the menu. Then it calls our wait_for_command to handle whatever user does next. Note that we picked the wording of our quick replies to match all possible regexp in wait_for_command. At the same time, user is not obliged to pick any of quick replies. He can shout at us something like ā€œGIVE ME COORDINATES YOU STUPID BOT!ā€ and his command will still be handled, as our regexp is case-insensitive. Now letā€™s add an else clause to our switch statement to handle the case where none of the commands known to us have been matched. Here is our new and improved wait_for_command:

Up until now, when the API request failed, user had to type something like ā€œcoordinatesā€ or ā€œfull addressā€ yet again to restart the whole process. That is not very cool, so letā€™s update our handle_api_request to retry the last command if API did not return any results. For that, we will use some metaprogramming (one of my favourite things about Ruby!). Look:

By theĀ thread

By now your bot should be testable. At last, we will adjust some thread settings, so our user is properly greeted and presented with ā€œGet Startedā€ button that allows to strike the conversation. We will also add a persistent menu that will give user constant access two both features, even if heā€™s in the middle of something else. Hereā€™s how it will look like:

Making our botĀ nicer

This will require us to use facebook-messengerā€™s Bot.onĀ :postback { |..| block } functionality. First, go to your Facebook developer console, under ā€œWebhooksā€ click ā€œEdit eventsā€œ and enable messaging_postbacks.

Add this to your bot.rb:

Now add some logic to handle postback events:

And thatā€™s all for PartĀ 2!

We have a fully functional (though may be not particularly sexy in order to attract VC millions) bot that we wrote in under 200 lines of pure Ruby code. Follow the third and the last part of this tutorial to see how we add some final bells and whistles and deploy to Heroku, so our Smooth Coordinator can finally go live.

All the code for our bot at this stage of its development can be found on my Github. You can find the code for a complete project (still WIP on the time of this writing) in the same repo, but in the main branch. Feel free to fork it, clone it and play with it.

GO TO PARTĀ 3

About me: Iā€™m a beginner web developer, ex senior international TV correspondent, and young father who is looking for a first proper programmer job. Aside from obsessing about coding style and trying to pump my head full of technical literature, I recently graduated top of the class from an excellent Le Wagon coding bootcamp in Paris and co-authored Halfway.ninja as a graduate project there. Itā€™s an airfare comparator for couples. Go check it out! You can find me on Github, Twitter and Instagram. I also own this domain. I am currently available for hire, be it an inspiring internship, freelance or even full-time work. I speak English, Russian, French, Ruby, JavaScript, Python and Swift and I am currently based in Moscow, Russian Federation. Feel free to write me directly with any offers.


Published by HackerNoon on 2016/12/21