Building a Trivia Bot for Facebook Messenger with Laravel (Part 2)
Hey there! So, we’ve been using Laravel to build a pretty basic Facebook Messenger bot that quizzes us on some general knowledge trivia. We did the basic setup of our bot in Part 1. In this part, we’ll finally get our bot to give us some real trivia challenges from the Open Trivia Database (http://opentdb.com). But first…
A few considerations
Facebook says,
It is extremely important to return a 200 OK HTTP as fast as possible. Facebook will wait for a 200 before sending you the next message. In high volume bots, a delay in returning a 200 can cause significant delays in Facebook delivering messages to your webhook.
To cater for that, we’ll make use of Laravel’s jobs feature: once we receive any data to our webhook, we respond with 200 OK and dispatch a new job where we process the message data. The job gets executed in the background. This is much better than hoping we can finish processing the data and respond within a few seconds.
Run php artisan make:job BotHandler
in your Terminal to create a new job. Configure your app’s queue by setting QUEUE_DRIVER=database in your Config Vars, and then running php artisan queue:table
and php artisan queue:failed-table
on your local, andphp artisan migrate
on Heroku. Then add this line to your Procfile:
queue: php artisan queue:work --tries=2
(See here for more details on using Laravel with Heroku.)
Facebook also says,
We may batch events in a single callback, especially during moments of high load. Be sure to iterate over
entry
andmessaging
in the response to capture all the events sent in the request
(Both quotes are from the Webhook Reference.)
To cater for this, we’ll have to make sure we iterate over all the entries sent and dispatch a job for each. Replace the code in the receive
method of your MainController
with something like this:
$entries = \App\Bot\Webhook\Entry::getEntries($request);
foreach ($entries as $entry) {
$messagings = $entry->getMessagings();
foreach ($messagings as $messaging) {
dispatch(new \App\Jobs\BotHandler($messaging));
}
}
return response();
The helper response()
generates an empty 200 OK response.
Modelling the data
Entry
here is a class we’ll create that models the structure of the entry
object in the data Facebook sends us (as explained here), and provides some handy methods for manipulating them. The getEntries()
method returns an array ofEntry
objects from the callback data in the Request. The Entry class will have a method called getMessaging()
which returns an array of Messaging
objects. Lastly, we’ll hae a Message
class to represent message events. We’ll ignore postback events for now. We’ll create all these classes.
Question: whats the use of all these extra classes? Couldn’t we just get what we need from parsing the input array?
Answer: Yes, we could. But apart from the fact that creating classes to model entities makes me feel like a developer that knows his OOP, it’s much better this way, as we don’t have to worry about the implementation details of the data.If Facebook changed the structure of data it sent, we’d only need to change the code inside our class definitions instead of changing it everywhere we accessed the array. So we’re future-proofing our code.
Let’s build these classes.
In your app folder, create a folder named Webhook in a Bot folder, then create the files Entry.php
, Messaging.php
and Message.php
. Here’s the structure we’ll start out with:
Entry.php:
<?php
namespace App\Bot\Webhook;
use Illuminate\Http\Request;
class Entry
{
private $time;
private $id;
private $messagings;
private function __construct(array $data)
{
$this->id = $data["id"];
$this->time = $data["time"];
$this->messagings = [];
foreach ($data["messaging"] as $datum) {
$this->messagings[] = new Messaging($datum);
}
}
//extracts entries from a Messenger callback
public static function getEntries(Request $request)
{
$entries = [];
$data = $request->input("entry");
foreach ($data as $datum) {
$entries[] = new Entry($datum);
}
return $entries;
}
public function getTime()
{
return $this->time;
}
public function getId()
{
return $this->id;
}
public function getMessagings()
{
return $this->messagings;
}
}
Messaging.php:
<?php
namespace App\Bot\Webhook;
class Messaging
{
public static $TYPE_MESSAGE = "message";
private $senderId;
private $recipientId;
private $timestamp;
private $message;
private $type;
public function __construct(array $data)
{
$this->senderId = $data["sender"]["id"];
$this->recipientId = $data["recipient"]["id"];
$this->timestamp = $data["timestamp"];
if(isset($data["message"])) {
$this->type = "message";
$this->message = new Message($data["message"]);
}
}
public function getSenderId()
{
return $this->senderId;
}
public function getRecipientId()
{
return $this->recipientId;
}
public function getTimestamp()
{
return $this->timestamp;
}
public function getMessage()
{
return $this->message;
}
public function getType()
{
return $this->type;
}
}
Here, type
(and its corresponding getType()
is an extra property we added to the Messaging class that helps us easily tell what type of message data we have, for instance “message” or “postback” (ignoring postback for now, though)
Message.php:
<? php
namespace App\Bot\Webhook;
class Message
{
private $mId;
private $text;
private $attachments;
private $quickReply;
public function __construct(array $data)
{
$this->mId = $data["mid"];
$this->text = isset($data["text"]) ? $data["text"] : "";
$this->attachments = isset($data["attachments"]) ? $data["attachments"] : "";
$this->quickReply = isset($data["quick_reply"]) ? $data["quick_reply"] : "";
}
public function getId()
{
return $this->mId;
}
public function getText()
{
return $this->text;
}
public function getAttachments()
{
return $this->attachments;
}
public function getQuickReply()
{
return $this->quickReply;
}
}
At the expense of having too much (?) code, I made all fields private and created getters to access them, so someone doesnt accidentally change their values.
Handling a Message Event
Okay, we’ve been able to set up useful formats for representing and accessing our data. It’s time for us to begin writing the code that handles a message event. Let’s edit our BotHandler
:
protected $messaging;
/**
* Create a new job instance.
*
* @return void
*/
public function __construct(Messaging $messaging)
{
$this->messaging = $messaging;
}
/** * Execute the job. * * @param Messaging $messaging */
public function handle()
{
if ($this->messaging->getType() == "message") {
$bot = new Bot($this->messaging);
$custom = $bot->extractDataFromMessage();
//a request for a new question
if ($custom["type"] == Trivia::$NEW_QUESTION) {
$bot->reply(Trivia::getNew($custom['user_id']));
} else if ($custom["type"] == Trivia::$ANSWER) {
$bot->reply(Trivia::checkAnswer($custom["data"]["answer"], $custom['user_id']));
} else {
$bot->reply("I don't understand. Try \"new\" for a new question");
}
}
}
You can see we make use of our Messaging
class’ getType()
method to determine if it is a message event. You probably also noticed the two new classes, Bot
and Trivia
. These two classes are very important. We’ll use the Bot
class to handle interactions between our bot, our data, and Messenger. We’ll use the Trivia
class to handle interactions with the OpenTriviaDatabase API, and any other functionality specific to trivia, such as verifying answers.
Some More Classes
Let’s create these classes.
First, we’ll add the $messaging
property and initialise it in the constructor.
private $messaging;
public function __construct(Messaging $messaging)
{
$this->messaging = $messaging;
}
This property is important because we don’t want to keep passing the id of the person to reply to,or the Messaging instance on each call to reply. The bot gets that internally from this property.
Next, we add the extractDataFromMessage()
method to the \App\Bot\Bot
class. This method will simply return an array with thre elements: type
, which contains the type of message in relation to our bot’s functionality (such as “request for a new question” or “answer”), data
, which contains any useful data we need from the message, such as the user’s answer, and user_id
, so we can identify which user sent the message. We’ll use regular expressions to achieve this:
public function extractDataFromMessage()
{
$matches = [];
$text = $this->messaging->getMessage()->getText();
//single letter message means an answer
if (preg_match("/^(\\w)\$/i", $text, $matches)) {
return [
"type" => Trivia::ANSWER,
"data" => ["answer" => $matches[0]],
"user_id" => $this->messaging->getSenderId()
];
} else if (preg_match("/^new|next\$/i", $text, $matches)) {
//"new" or "next" requests a new question
return [
"type" => Trivia::NEW_QUESTION,
"data" => [],
"user_id" => $this->messaging->getSenderId()
];
}
//anything else, we dont care
return [
"type" => "unknown",
"data" => [],
"user_id" => $this->messaging->getSenderId()
];
}
Lastly, we’ll implement the reply
method, which will be used to reply to messages. Note that it takes only one argument, the data to reply with. We obtain the id of the user to reply to from the senderId
property of our $messaging
object. The function then calls the sendMessage()
method, which uses curl to make a POST request containing our message data to Facebook’s Send API.
public function reply($data)
{
if (method_exists($data, "toMessengerMessage")) {
$data = $data->toMessengerMessage();
} else if (gettype($data) == "string") {
$data = ["text" => $data];
}
$id = $this->messaging->getSenderId();
$this->sendMessage($id, $data);
}
private function sendMessage($recipientId, $message)
{
$messageData = ["recipient" => ["id" => $recipientId], "message" => $message];
$ch = curl_init('https://graph.facebook.com/v2.6/me/messages?access_token=' . env("PAGE_ACCESS_TOKEN"));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HEADER, false);
curl_setopt($ch, CURLOPT_HTTPHEADER, ["Content-Type: application/json"]);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($messageData));
Log::info(print_r(curl_exec($ch), true));
}
Lets take a closer look at these lines:
if (method_exists($data, "toMessengerMessage")) {
$data = $data->toMessengerMessage();
} else if (gettype($data) == "string") {
$data = ["text" => $data];
}
In other words, if the data the function is provided with is an object with a toMessengerMessage()
method, we call that method and receive a message array in the format Facebook expects. If the data is a string, we just use it as the text
property in the message array. This allows us to pass in custom objects containing structured messages as well as plain text to our reply function.
Now let’s create the Trivia class.
First, well define the constants we used earlier, and set up a constructor to initalise the parts of a Trivia: question and answers.
namespace App\Bot;
use Illuminate\Support\Facades\Cache;
class Trivia
{
const NEW_QUESTION = "new";
const ANSWER = "answer";
public $question;
public $options;
private $solution;
private $userId;
public function __construct(array $data, $userId)
{
$this->question = "";
$this->solution = "";
$this->options = [];
$this->userId = userId;
}
}
For now, we initialise all three to empty values.
Let’s implement the getNew()
method. First, we’ll clear any existing solution to an earlier question, if any, from the cache and then we’ll make an API call to get the question. We’ll then create a Trivia object and return it.
public static function getNew($userId)
{
//clear any past solutions for this user left in the cache
Cache::forget("solution.$userId");
//make API call and decode result to get general-knowledge trivia question
$ch = curl_init("https://opentdb.com/api.php?amount=1&category=9&type=multiple");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HEADER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
$result = json_decode(curl_exec($ch), true)["results"][0];
return new Trivia($result, $userId);
}
Since this Trivia
object is passed to the $bot->reply()
method, we need to implement a toMessage()
method. Let’s do that now.
The toMessage()
method should return an array that fits into the message
key of the JSON object we send to Facebook, as detailed here. For now, we’ll just send a basic text message, stating the question and listing the options. We’ll also be storing the letter for the correct answer in our cache too, so as to persist it.
public function toMessage()
{
//compose message
$response = "Question: $this->question.\nOptions:";
$letters = ["a", "b", "c", "d"];
foreach ($this->options as $i => $option) {
$response .= "\n{$letters[$i]}: $option";
if ($this->solution == $option) {
Cache::forever("solution.{$this->userId}", $letters[$i]);
}
}
return ["text" => $response];
}
Almost done. Let’s head back to our constructor and fill in the missing pieces. If you visit our API URL, https://opentdb.com/api.php?amount=1&category=9&type=multiple, in your browser, you’ll see a response like this (I’ve formatted it a bit to be more readable):
{
"response_code":0,
"results": [
{
"category": "General Knowledge",
"type":"multiple",
"difficulty":"...",
"question":"...",
"correct_answer":"...",
"incorrect_answers": [...]
}
]
}
Based on this, we can update our constructor to look like this (remember that $data
is ["results"][0]
):
public function __construct(array $data, $userId)
{
$this->question = $data["question"];
$answer = $data["correct_answer"];
$this->options = $data["incorrect_answers"];
$this->options[] = $answer;
shuffle($this->options); //shuffle the options, so we don't always present the right answer at a fixed place
$this->solution = $answer;
$this->userId = $userId;
}
Finally, we’ll implement the function that checks if the user’s answer is correct and returns a text response:
public static function checkAnswer($answer, $userId)
{
$solution = Cache::get("solution.$userId");
if ($solution == strtolower($answer)) {
$response = "Correct!";
} else {
$response = "Wrong. Correct answer is $solution";
}
//clear solution
Cache::forget("solution.$userId");
return $response;
}
Done!
Note: See this article for how to configure cache, queuing and database for Laravel on Heroku.
Lets test out our bot now:
Yes, it works!
Thanks for sticking with me! Hope you enjoyed this! In Part 3, we’ll implement some of the cool features Messenger gives such, such as quick replies and message buttons. Stay tuned!
Github repo for the bot is here.
If you liked this article, please recommend and share. And if you spot any needed improvement, just suggest it in the comments. I’d appreciate it. Thanks!
Next: Adding Messenger Platform features
Part 4: Modifying our bot’s Messenger Profile
Part 5: Personalising the User Experience
Useful Links:
Webhook Reference - Messenger Platform - Documentation - Facebook for Developers
Queues - Laravel - The PHP Framework For Web Artisans
Cache - Laravel - The PHP Framework For Web Artisans
I write about my software engineering learnings and experiments. Stay updated with Tentacle: tntcl.app/blog.shalvah.me.