Building a bot for Facebook Messenger, using Elixir
This post will go over a practical implementation of writing a bot for Messenger, using Elixir.
The bot we’re going to write is called Bastion, a hero from Blizzard’s new game Overwatch. Bastion is a simple machine with a limited set of responses.
If Bastion receives any text message, he’ll reply with a message containing some help text and options for the user to select:
- Your name
- Your website
This should provide enough scope to explain some of the basics about creating a bot for Messenger, and some of the different message types.
Setting up the Facebook app
As we’re building a bot on top of Messenger, we need to do some setup at the Facebook side of things.
Create your app
Head over to Messenger and click Start Building. You’ll need to sign in to a Facebook account in order to access it.
In the top-right corner, click My Apps > Add a New App. You want to select a Website app, and fill in the rest of the details (such as name and contact email address).
Add a new app
You can skip quick start as you don’t need any of those steps. Towards the bottom of the left-hand menu, choose Messenger, and click Get Started.
Create a page for your app
This step is straightforward. All you need to do is Create a Page, or you can use an existing page if you have one.
Verify the bot
Now we actually have to start writing some code. We’ll need a basic web server that can respond to a request which Facebook is going to make to verify the URL of our bot.
Start off a new mix application:
mix new bastion --sup
We’re going to be using cowboy and Plug for my web server and router.
Add those to your mix.exs
file as dependencies and run mix deps.get
:
def application do
[applications: [:logger, :cowboy, :plug],
mod: {Bastion, []}]
end
defp deps do
[{:cowboy, "~> 1.0"},
{:plug, "~> 1.0"}]
end
We need a module that will create our server, as well as a module that will be our router.
The server module is fairly straight forward, and the port can be configured (for Heroku’s benefit):
defmodule Bastion.Server do
@default_port 4000
def start_link(nil), do: start_server(@default_port)
def start_link(port), do: port |> String.to_integer() |> start_server()
defp start_server(port) do
Plug.Adapters.Cowboy.http Bastion.Router, [], port: port
end
end
We also need to add this module as a worker
to the application supervision
tree, in lib/bastion.ex
:
children = [
worker(Bastion.Server, [System.get_env("PORT")])
]
You can see here we’re passing in the PORT
environment variable, and with
pattern matching the server module will handle it accordingly.
In the Bastion.Server
module you’ll notice we used a Bastion.Router
module.
We’ll write that next.
For Facebook to verify our web server, we need to respond to a request at a
specific endpoint. It can be whatever you like, but ours is going to be
/webhook
. In order to verify, you need to echo back the incoming
hub.challenge
query string. You should also check that the incoming request
is from Facebook, which can be done by checking hub.verify_token
. You can
also customise the verify token, but for this example we’ll be using
secret-token
.
defmodule Bastion.Router do
use Plug.Router
plug Plug.Logger
plug :match
plug :dispatch
get "/webhook" do
conn = Plug.Conn.fetch_query_params(conn)
if conn.params["hub.verify_token"] == "secret-token" do
send_resp(conn, 200, conn.params["hub.challenge"])
else
send_resp(conn, 401, "Error, wrong validation token")
end
end
match _ do
send_resp(conn, 404, "404 - Page not found")
end
end
There are a few quality of life additions here that aren’t required in order to
get this working, such as Plug.Logger
(which logs all requests to the
terminal) and the match
function towards the end of the router, which will
catch any missed requests and return a 404.
With all that in place I’m going to deploy the bot to Heroku at https://bastion-bot.herokuapp.com, using heroku-buildpack-elixir.
Head back to your Messenger dashboard, and click on Setup Webhooks. You
need to provide the URL to your endpoint, the verify token that you set up in
your app (secret-token
in this example), and just check all of the
subscription fields.
Subscribe your app to the page
Our final step is to subscribe the app to the page we created. Subscribing means that any events generated by the page (such as messages) will be sent to our bot.
The Getting Started guide states that you need to send a cURL request to subscribe, but there’s actually a useful button within the Webhooks box. Just chose your page and click Subscribe.
If that doesn’t work (I’ve had cases where it doesn’t), then you’ll need to use
the cURL method. To do that, at the top of the Messenger dashboard you can
select your page and it will give you the Page Access Token. Run the
following, replacing <page access token>
with your own:
curl -ik -X POST "https://graph.facebook.com/v2.6/me/subscribed_apps?access_token=<page access token>"
You should see a {“success”: true}
returned in the response.
Now we can start sending and receiving messages from our bot.
Our first message
When someone sends a message to our bot, we’re going to receive a POST
to the
same endpoint we used for verification. The payload will be in JSON so we’re
going to have to decode it first.
To do that we’re going to need another dependency, and I’ll be using
Poison. While we’re at it, lets grab HTTPoison too which we’ll need
later on in order to send HTTP requests back to Facebook. Our updated
dependencies in mix.exs
looks like this:
def application do
[applications: [:logger, :cowboy, :plug, :httpoison],
mod: {Bastion, []}]
end
defp deps do
[{:cowboy, "~> 1.0"},
{:plug, "~> 1.0"},
{:poison, "~> 2.1"},
{:httpoison, "~> 0.8.3"}]
end
Don’t forget to run mix deps.get
.
We need to create the POST route for our endpoint, parse the incoming JSON,
then do something with the message. We’re going to delegate any incoming
messages to a module called MessageHandler
. That way our router isn’t doing
too much work.
The incoming JSON payload is quite complex, and you can find the schema in the
webhook reference documentation. I’m going to provide you with a
simplified version of the code I use to parse incoming messages and call
MessageHandler.handle/1
with each message:
post "/webhook" do
{:ok, body, conn} = Plug.Conn.read_body(conn)
body
|> Poison.Parser.parse!(keys: :atoms)
|> Map.get(:entry)
|> hd()
|> Map.get(:messaging)
|> Enum.each(&Bastion.MessageHandler.handle/1)
send_resp(conn, 200, "Message received")
end
Now we just need to do something with the message in the MessageHandler
module. Through the magic of pattern matching, we’re going to have different
implementations of MessageHandler.handle/1
depending on the incoming message.
What we want to do first is match on any incoming text message, and send back a
message with the buttons.
defmodule Bastion.MessageHandler do
require Logger
def handle(msg = %{message: %{text: _text}}) do
# incoming text message
end
def handle(msg) do
Logger.info "Unhandled message:\n#{inspect msg}"
end
end
This is the outline of the module. We have a function that matches on any text messages (we don’t actually care what the text is though), and a fallback function that will log any incoming messages that we aren’t handling.
To send a message back to the user, we need to use the [Send API][14]. We’re going to use what is called the Button Template to send a message to the user with our buttons. You’re going to need your Page Access Token from the Messenger dashboard for this part:
@fb_page_access_token System.get_env("FB_PAGE_ACCESS_TOKEN")
def handle(msg = %{message: %{text: _text}}) do
buttons = [
%{type: "postback", title: "Your name", payload: "PB_NAME"},
%{type: "web_url", title: "Your website", url: "https://playoverwatch.com/en-us/heroes/bastion/"}
]
send_button_message(msg.sender.id, "Choose from the following options", buttons)
end
defp send_button_message(recipient, text, buttons) do
payload = %{
recipient: %{id: recipient},
message: %{
attachment: %{
type: "template",
payload: %{
template_type: "button",
text: text,
buttons: buttons
}
}
}
}
url = "https://graph.facebook.com/v2.6/me/messages?access_token=#{@fb_page_access_token}"
headers = [{"Content-Type", "application/json"}]
HTTPoison.post!(url, Poison.encode!(payload), headers)
end
If you’re using Heroku, make sure that you’ve set the environment variable
FB_PAGE_ACCESS_TOKEN
before deploying, and also added it to the list of
exported variables in elixir_buildpack.config
.
Now we can deploy our bot and try it out on the Facebook Page.
Handling the postback
So the message has gone to the user, and there are buttons they can click.
Luckily for us, we don’t need to do anything for the Your website button,
as Messenger handles that for us (web_url
is a special type of button that
will open the specified URL when pressed).
So we need to handle what happens when the user clicks the Your name
button. When we sent the button to the user, we included a payload, which was
PB_NAME
. When the user clicks the button, Messenger will send us the button’s
payload, which is how we’ll know which button was pressed.
For our MessageHandler
this is easily done with pattern matching, like so:
def handle(msg = %{postback: %{payload: "PB_NAME"}}) do
# incoming postback
end
In response to the button being sent, we’re going to send a simple text message back to the user with Bastion’s real name:
def handle(msg = %{postback: %{payload: "PB_NAME"}}) do
send_text_message(
msg.sender.id,
"My name is SST Laboratories Siege Automation E54, otherwise known as Bastion"
)
end
defp send_text_message(recipient, text) do
payload = %{
recipient: %{id: recipient},
message: %{
text: text
}
}
url = "https://graph.facebook.com/v2.6/me/messages?access_token=#{@fb_page_access_token}"
headers = [{"Content-Type", "application/json"}]
HTTPoison.post!(url, Poison.encode!(payload), headers)
end
Once you click on the Your name button you should get your text message back from the bot.
That’s all folks
So there you have it, creating a bot for Messenger. From here you can look at the other types of messages you can send, or use this as a base to build your own bot.
I hope you found this post useful. If you did, please leave a comment.