How I Built A Telegram Quiz Chatbot With BotMan and Laravel

Chimeremze Prevail Ejimadu
12 min readFeb 23, 2023

--

Photo by Christian Wiediger on Unsplash

Hey everyone. In this article, I want to show you how I built the Chatbot @laraquizprobot on telegram with Laravel and BotMan. If you haven't tried it yet, it is a good idea to check it out before reading this article. This will help to understand what I am talking about.

The chatbot provides a quiz with all kinds of questions from four different tracks including Laravel framework, Django, React JS and CSS. Every question comes with up to three possible answers. You need to pick the right one to collect points and make it to the highscore. But besides the ranks, the quiz is about having a good time. Enjoy the questions and see what you know about your favorite framework. Have fun!

Note: All the code can be found in the public repository.

Step 1: Install a fresh laravel application and Botman packages

You can easily integrate Botman into an existing laravel application, but for the sake of this tutorial, let’s install a new laravel application. We can use composer or the laravel installer.
Just run the command below to install it. Since I used Laravel 8 for this, let’s install the same

composer create-project --prefer-dist laravel/laravel quizbot 8.*

Next, let’s install Botman.

composer require botman/botman

Finally, let’s install BotMan Telegram Driver.

composer require botman/driver-telegram

Now we’ve installed everything we need to get started. So next, let’s understand the project we want to create. If you’ve seen the bot in action, then you’ll get it.

We need three models — Track model, Question model, Answer model. We will create others as we need them.

Step 2: Create the Models and migrations.

First, Use the Code below to create the 5 models we need for our application, it will create their migration files along side:

php artisan make:model Track -m
php artisan make:model Question -m
php artisan make:model Answer -m
php artisan make:model Highscore -m
php artisan make:model Played -m

Next, open their Migrations respective and add the codes below to create the tables we need.

// TRACK Migration
Schema::create('tracks', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->timestamps();
});

// Questions Migration
Schema::create('questions', function (Blueprint $table) {
$table->id();
$table->string('text');
$table->integer('points')->unsigned();
$table->integer('track_id');
$table->timestamps();
});

// Answer Migration
Schema::create('answers', function (Blueprint $table) {
$table->id();
$table->integer('question_id');
$table->text('text');
$table->boolean('correct_one');
$table->timestamps();
});

// Highscore Migration
Schema::create('highscores', function (Blueprint $table) {
$table->id();
$table->integer('chat_id');
$table->string('name');
$table->integer('points')->default(0);
$table->integer('correct_answers')->default(0);
$table->integer('tries')->default(0);
$table->timestamps();
});

// Played Migration
Schema::create('playeds', function (Blueprint $table) {
$table->id();
$table->string('chat_id');
$table->integer('points')->default(0);
$table->timestamps();
});

Step 3: Add Database Seeder and run migrations

I decided to use a Laravel Seeder class to do so. This way I can keep all questions and answers inside the repository and use an artisan command to fill the database.

You can create a seeder separately for this, but I used the default for this. In the database/seeders/DatabaseSeeder.php,

In the run method of the seeder, I truncate the tracks table, create the tracks with the addTracks() method, then I loop through each track and add questions and to the questions, I add the answers.

For the actual data, I use a separate getData() method to keep it cleaner. In this method, I return a big collection with all the questions and answers. For better reading, I only show three of them here. As you can see, the code is quite straightforward. It is just a big array with all the data for questions and answers. Don’t forget to import the namespaces for the Track, Answer and Question classes.

Note: You’ll get the entire questions in the public repo.

Now all we need to do is to run migration. Make sure your database details is properly add in the .env file. Then run the command below to create and seed the tables at once.

php artisan migrate --seed

We now have our tracks, question and answers in our database.

Step 4: Create the BotManController

Now, let’s create a controller that we’ll use to bind our Conversations to our route.

Run the command below in your terminal:

php artisan make:controller BotManController

Let’s open the controller, create a method called handle() and add the following code

<?php

namespace App\Http\Controllers;

use BotMan\BotMan\BotMan;
use BotMan\BotMan\BotManFactory;
use BotMan\BotMan\Cache\LaravelCache;
use BotMan\BotMan\Drivers\DriverManager;

class BotManController extends Controller
{
/**
* Place your BotMan logic here.
*/
public function handle()
{
DriverManager::loadDriver(\BotMan\Drivers\Telegram\TelegramDriver::class);

$config = [
'user_cache_time' => 720,

'config' => [
'conversation_cache_time' => 720,
],

"telegram" => [
"token" => env('TELEGRAM_TOKEN'),
]
];

// // Create BotMan instance
$botman = BotManFactory::create($config, new LaravelCache());

$botman->hears('hello', function (BotMan $bot) {
$bot->reply('Hello yourself.');
});

$botman->listen();
}
}

Before we go further, I want to check if we are set to go. What’s happening in this controller is that I initiated a Botman instance and then set it to listen for ‘hello’, if it does hear ‘hello’, it should respond with ‘hello yourself’.

We have to connect our bot to telegram, and to do this, we need to expose our localhost. When I built this bot, I used BeyondCode’s Expose to share my local site via secure tunnel. you can use laravel valet or services like nginx.

You have to learn how to create a Telegram bot. When you create this, you get a token. Add that token to your .env file as

TELEGRAM_TOKEN=your-telegram-token-here

If you look at our controller, you’ll see that in the $config, we are calling the telegram token from our .env via env(‘TELEGRAM_TOKEN’).

Create your route

Let us quickly create our route. It serves as the starting point of our communication with the bot. inside the routes/web.php add:

use App\Http\Controllers\BotManController;

Route::match(['get', 'post'], 'botman', [BotManController::class, 'handle']);

Good. We have an endpoint the points to the handle() method in our BotManController.

Let’s go on to test our setup so far. Make sure you have exposeed your local server through a secure tunnel using valet, nginx or expose, or anyother similar service.

Register your webhook

Then run the artisan command to help you register your webhook:

php artisan botman:telegram:register

Then put in the secure link provided you and add the endpoint we created ‘/botman’ to it. Just like ‘https://your-secure-link.com/botman’. if the response is okay, it means your webhook have been registered to telegram successfully.

Wait!

You need to add the endpoint to the exception in the app/Http/Middleware/VerifyCsrfToken.php

// app/Http/Middleware/VerifyCsrfToken.php

protected $except = [
'botman'
];

Now go to telegram, to the bot you created, mine is @laraquizprobot, and type ‘hello’. If you get a reply ‘Hello there’, then you’re good to go.

If you do not get any reply, check the steps to make sure you didn’t miss anything, you can also check the repo to catch up.

Great! Let’s move to the Conversations.

Step 5: Create the Quiz Conversation.

Now that we can communicate with our bot on telegram, let’s create conversations. The first conversation we’ll create will be the Quiz Conversation. All our conversations will live in the app/Conversations, so create a new file called QuizConversation.php there with this code.

app/Conversations/QuizConversation.php

<?php

namespace App\Conversations;

use BotMan\BotMan\Messages\Conversations\Conversation;

class QuizConversation extends Conversation
{


/**
* Start the conversation.
*
* @return mixed
*/
public function run()
{
}
}

The questions are stored in the database. We want to grab them and ask the user each of them. First off, the user has to select a track, then the bot will introduce the quiz and then start asking questions from that track and show the answers if they are wrong or right. So let’s modify our QuizConversation.

Note: The run() method is the method that runs when the conversation is called.

<?php

namespace App\Conversations;

use App\Models\Track;
use BotMan\BotMan\Messages\Outgoing\Actions\Button;
use BotMan\BotMan\Messages\Conversations\Conversation;
use BotMan\BotMan\Messages\Incoming\Answer as BotManAnswer;
use BotMan\BotMan\Messages\Outgoing\Question as BotManQuestion;

class QuizConversation extends Conversation
{
/** @var Track */
protected $quizTracks;

/** @var Question */
protected $quizQuestions;

/** @var integer */
protected $userPoints = 0;

/** @var integer */
protected $userCorrectAnswers = 0;

/** @var integer */
protected $questionCount;

/** @var integer */
protected $currentQuestion = 1;


/**
* Start the conversation.
*
* @return mixed
*/
public function run()
{
$this->quizTracks = Track::all();
$this->selectTrack();
}


private function selectTrack()
{
$this->say(
"We have " . $this->quizTracks->count() . " tracks. \n You have to choose one to continue.",
['parse_mode' => 'Markdown']
);
$this->bot->typesAndWaits(1);

return $this->ask($this->chooseTrack(), function (BotManAnswer $answer) {
$selectedTrack = Track::find($answer->getValue());

if (!$selectedTrack) {
$this->say('Sorry, I did not get that. Please use the buttons.');
return $this->selectTrack();
}

return $this->setTrackQuestions($selectedTrack);
}, [
'parse_mode' => 'Markdown'
]);
}
}

The first thing the conversation does is to set $this->quizTracks, and then run the selectTrack() method. This method asks the user to choose a track, and displays the number of tracks and the options available. It takes the users answer and based on it, it called the next function $this->setTrackQuestions($selectedTrack) to set the question for the selected track and then move to the next method.

Because of the actions performed here, I will explains some of the key functions. You can grab the entire code of this conversation here

We shuffle the questions and switch the collection’s key to the id. This will make it easier for us to remove items from the collection. Before showing a question, we check if there are any left. After every question we will remove it. When there are no more questions left, we show the user the quiz result.

setTrackQuestions() and askQuestion() methods in QuizConversation

Here is the askQuestion method. It always gets the first item of the questions collections and uses the BotMan ask method to show the text to the user, as well as the answers. We loop over the question's answers, to add a button for each of them to the template. We are also checking if the user’s reply is from a button click. If not, I will repeat this question. This is possible because the buttons have the answer’s id as the value. We use it to find the answer.

When a user completes a quiz, the next conversation is the High-score conversation.

Highscore

The Highscore model will include a few things. First, we define the fillable property to whitelist what values we can store later. Also, the table name needs to be defined here because it differs from the traditional names.

protected $fillable = ['chat_id', 'name', 'points', 'correct_answers', 'tries'];
protected $table = 'highscore';

Then we add a method for storing highscore entries. I use the updateOrCreate method to prevent duplicate entries. Every user should only have one entry. With every new try, the highscore will get updated. The unique field for these entries is the chat_id and not, like normally, the id field. The updateOrCreate method gets this info as the first parameter. Also note how I increase the tries field if the entry is not a new one. For this, the wasRecentlyCreated method comes in handy.

public static function saveUser(UserInterface $botUser, int $userPoints, int $userCorrectAnswers)
{
$user = static::updateOrCreate(['chat_id' => $botUser->getId()], [
'chat_id' => $botUser->getId(),
'name' => $botUser->getFirstName().' '.$botUser->getLastName(),
'points' => $userPoints,
'correct_answers' => $userCorrectAnswers,
]);
	$user->increment('tries');	$user->save();	return $user;
}

At the end of the quiz, we show the user his rank. This is what the next method is for. We count how many users have more points than the current one. We are only interested in the unique values because users with the same count will share a rank.

public function getRank()
{
return static::query()->where('points', '>', $this->points)->pluck('points')->unique()->count() + 1;
}

For the highscore, only the top 10 users will be shown.

public static function topUsers($size = 10)
{
return static::query()->orderByDesc('points')->take($size)->get();
}

We fetch the users with the most points and add a rank field to every entry.

Here’s our final Highscore Model

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use BotMan\BotMan\Interfaces\UserInterface;

class Highscore extends Model
{
use HasFactory;
protected $fillable = ['chat_id', 'name', 'points', 'correct_answers', 'tries'];

public static function saveUser(UserInterface $botUser, int $userPoints, int $userCorrectAnswers)
{
$user = static::updateOrCreate(['chat_id' => $botUser->getId()], [
'chat_id' => $botUser->getId(),
'name' => $botUser->getFirstName().' '.$botUser->getLastName(),
'points' => $userPoints,
'correct_answers' => $userCorrectAnswers,
]);

$user->increment('tries');

$user->save();

return $user;
}

public function getRankAttribute()
{
return static::query()->where('points', '>', $this->points)->pluck('points')->unique()->count() + 1;
}

public static function topUsers($size = 15)
{
return static::query()->orderByDesc('points')->take($size)->get();
}

public static function deleteUser(string $chatId)
{
Highscore::where('chat_id', $chatId)->delete();
}
}

Showing the highscore

To show our highscore, we create a conversation for it at app/Conversations/HighscoreConversation.php

<?php

namespace App\Conversations;

use App\Models\Highscore;
use BotMan\BotMan\Messages\Outgoing\Actions\Button;
use BotMan\BotMan\Messages\Conversations\Conversation;
use BotMan\BotMan\Messages\Incoming\Answer as BotManAnswer;
use BotMan\BotMan\Messages\Outgoing\Question as BotManQuestion;

class HighscoreConversation extends Conversation
{
/**
* Start the conversation.
*
* @return mixed
*/
public function run()
{
$this->showHighscore();
}

private function showHighscore()
{
$topUsers = Highscore::topUsers();

if (! $topUsers->count()) {
return $this->say('The highscore is still empty. Be the first one! 👍');
}

$topUsers->transform(function ($user) {
return "_{$user->rank} - {$user->name}_ *{$user->points} points*";
});

$this->say('Here is the current highscore showing the top 15 results.');
$this->bot->typesAndWaits(1);
$this->say('🏆 HIGHSCORE 🏆');
$this->bot->typesAndWaits(1);
$this->say($topUsers->implode("\n"), ['parse_mode' => 'Markdown']);
$this->bot->typesAndWaits(2);
$this->say("If you want to play another round click: /start \nOne of the ways to improve what you know about Laravel is by going hrough their documentation at https://laravel.com/docs/.");
}

}

How to initiate a conversation

To trigger this and other conversations, we need listeners. Besides the start keyword I also add /start which will become a Telegram command later. I’ll do same for highscore.

// appHttp/Controllers/BotManController.php

$botman->hears('start|/start', function (BotMan $bot) {
$bot->startConversation(new QuizConversation());
})->stopsConversation();

$botman->hears('/highscore|highscore', function (BotMan $bot) {
$bot->startConversation(new HighscoreConversation());
})->stopsConversation();

To test, you can type one of the keywords to the bot to see what happens. It will tell you that the highscore is still empty.

Delete User Data

Since the GDPR it is essential to give the user the possibility to delete his stored data. I want to handle that in a new app/Conversations/PrivacyConversation.php class. First we make sure we have stored this current user. If so, we ask him if he really wants to delete his highscore entry.

<?phpnamespace App\Conversations;use App\Highscore;
use BotMan\BotMan\Messages\Incoming\Answer;
use BotMan\BotMan\Messages\Outgoing\Question;
use BotMan\BotMan\Messages\Outgoing\Actions\Button;
use BotMan\BotMan\Messages\Conversations\Conversation;
class PrivacyConversation extends Conversation
{
/**
* Start the conversation.
*
* @return mixed
*/
public function run()
{
$this->askAboutDataDeletion();
}
private function askAboutDataDeletion()
{
$user = Highscore::where('chat_id', $this->bot->getUser()->getId())->first();

if (! $user) {
return $this->say('We have not stored any data of you.');
}

$this->say('We have stored your name and chat ID for showing you in the highscore.');
$question = Question::create('Do you want to get deleted?')->addButtons([
Button::create('Yes please')->value('yes'),
Button::create('Not now')->value('no'),
]);

$this->ask($question, function (Answer $answer) {
switch ($answer->getValue()) {
case 'yes':
Highscore::deleteUser($this->bot->getUser()->getId());
return $this->say('Done! Your data has been deleted.');
case 'no':
return $this->say('Great to keep you 👍');
default:
return $this->repeat('Sorry, I did not get that. Please use the buttons.');
}
});
}
}

The deleteUser is a new Highscore model method you have to add.

public static function deleteUser(string $chatId)
{
Highscore::where('chat_id', $chatId)->delete();
}

About

If someone wants to know more about this bot, I will give him the possibility by adding some about info. Since I only return one little text message, I will handle it inside the listener.

$botman->hears('/about|about', function (BotMan $bot) {
$bot->reply('This is a BotMan and Laravel 8 project by Ejimadu Prevail.');
})->stopsConversation();

Our Controller will now look like this :

<?php

namespace App\Http\Controllers;

use BotMan\BotMan\BotMan;
use App\Conversations\QuizConversation;
use App\Conversations\PrivacyConversation;
use App\Conversations\HighscoreConversation;
use App\Http\Middleware\PreventDoubleClicks;
use BotMan\BotMan\BotManFactory;
use BotMan\BotMan\Cache\LaravelCache;
use BotMan\BotMan\Drivers\DriverManager;

class BotManController extends Controller
{
/**
* Place your BotMan logic here.
*/
public function handle()
{
DriverManager::loadDriver(\BotMan\Drivers\Telegram\TelegramDriver::class);

$config = [
'user_cache_time' => 720,

'config' => [
'conversation_cache_time' => 720,
],

"telegram" => [
"token" => env('TELEGRAM_TOKEN'),
]
];

// // Create BotMan instance
$botman = BotManFactory::create($config, new LaravelCache());

$botman->middleware->captured(new PreventDoubleClicks);

$botman->hears('start|/start', function (BotMan $bot) {
$bot->startConversation(new QuizConversation());
})->stopsConversation();

$botman->hears('/highscore|highscore', function (BotMan $bot) {
$bot->startConversation(new HighscoreConversation());
})->stopsConversation();

$botman->hears('/about|about', function (BotMan $bot) {
$bot->reply('This is a BotMan and Laravel 8 project by Ejimadu Prevail.');
})->stopsConversation();

$botman->hears('/deletedata|deletedata', function (BotMan $bot) {
$bot->startConversation(new PrivacyConversation());
})->stopsConversation();

$botman->listen();
}
}

Conclusion

This article got longer than I expected, but there’s still a lot to cover on how to build dynamic chatbots using PHP. This article will help you to build your own chatbot as it covered most of the concepts in BotMan 2.0. You can always check out the repo, and try out different things as much as possible.

Stay tuned!!! I will be back with some more cool Laravel tutorials in the next article. I hope you liked the article. Don’t forget to follow me 😇 and give some clap 👏. And if you have any questions feel free to comment.

Thank you.

--

--

Chimeremze Prevail Ejimadu

Laravel Developer + Writer + Entrepreneur + Open source contributor + Founder + Open for projects & collaborations. Hit FOLLOW ⤵