`I was tasked to build a trivia chat-bot, and the upper management wanted to see a POC (Proof of Concept) by end of the day. Which means I had around 3 hours to build this POC. But there was one small problem, I had no idea how this is done.
In this installment of our series TIL (Today I Learned), I will be writing about the story of how I was able to build a decent Quiz bot on telegram in less than 1 hour. (I wrote this post and watched some Youtube videos in the remaining 2 hours of my day)
As any software developer in this planet, I first went to GitHub to see if there is any code that I can straight up copy. But unfortunately, then once that were decent was like 2 years old. I started reading them and got to know about this node.js library for Telegram Bots API called Telegraf.
Then I quickly built a fresh folder in my Projects directory and initiated npm.
npm init -y
After that, I installed telegraf (obviously)
npm i telegraf
Copy pasted the example code provided by Telegraf and realised there is a problem.
const { Telegraf } = require('telegraf')
const bot = new Telegraf(process.env.BOT_TOKEN)
bot.start((ctx) => ctx.reply('Welcome!'))
bot.help((ctx) => ctx.reply('Send me a sticker'))
bot.on('sticker', (ctx) => ctx.reply('👍'))
bot.hears('hi', (ctx) => ctx.reply('Hey there'))
bot.launch()
I don't actually have a "BOT TOKEN", the search of which led to me to this Telegram Bot called BotFather which made it incredibly easy to get the BOT API Token.
After that I created an environment variable called BOT_TOKEN
and assigned the Telegram token to it. Then I started the node serer by
node index.js
Searched my BOT on Telegram and ...
So, 10 mins into code I have a basic chat bot ready. Since this was just a proof of concept, I didn't want to spend time of setting up database and stuff so I created a dictionary of current players and questions.
const questions = [
{
"question": "The tree sends down roots from its branches to the soil is know as:",
"o1": "Oak",
"o2": "Pine",
"o3": "Banyan",
"answer": "o3"
}
]
let players = {}
For I had 5 questions, but for this blog, there is only one. Also, I will initiate the players dynamically later on.
In telegram, you can send predefined commands such as /start
or /help
and the chatbot manager can send appropriate replies. For this bot I decided, I will send this message whenever a new user sends me /start
command.
bot.start((ctx)=>{
ctx.reply(`Hello, Are you ready to start the quiz?\nThere will be ${questions.length} question, each scoring 1 points.`)
const logo = fs.readFileSync('img/logo.jpg')
ctx.telegram.sendPhoto(ctx.chat.id, {
source: logo
}).catch(reason => {
console.log(reason)
})
})
Basically, I wrote a custom function on the example code that telegraf provided. Now the chatbot will behave like this.
The code you're gonna see next is shit and redundant, that's what being a senior develop is all about.
~ Chris (10 years of experience in Software Engineering)
Whenever the bot gets a message from a user, I want the manager to check if this player is new or have registered to the system. This can be done using a simple if condition since we are using in memory dictionaries.
bot.on('message', (ctx) => {
let userId = ctx.message.from.id;
if (userId in players){
if (players[userId]['last_question'] >= questions.length){
quiz_end_message = `You've completed the trivia ❤️❤️❤️, you scored ${players[userId].score} out of ${questions.length}.`
const keyboard = Markup.inlineKeyboard([
Markup.urlButton('Click here to get the Prizes', 'https://idiomaticprogrammers.com/'),
])
ctx.reply(quiz_end_message, Extra.markup(keyboard))
} else {
const keyboard = Markup.inlineKeyboard([
[Markup.callbackButton(questions[players[userId]['last_question']].o1,"o1")],
[Markup.callbackButton(questions[players[userId]['last_question']].o2,"o2")],
[Markup.callbackButton(questions[players[userId]['last_question']].o3,"o3")]
])
ctx.reply(questions[players[userId]['last_question']].question, Extra.markup(keyboard))
}
} else {
players[userId] = {
"last_question": 0,
"last_answer_time": new Date().getTime(),
"score": 0
}
console.log(userId)
const keyboard = Markup.inlineKeyboard([
[Markup.callbackButton(questions[0].o1,"o1")],
[Markup.callbackButton(questions[0].o2,"o2")],
[Markup.callbackButton(questions[0].o3,"o3")]
])
ctx.reply(questions[0].question, Extra.markup(keyboard))
}
})
Just read the code, I'll explain what's going on in a minute.
First of all, we can get the ID of the user who just messaged us using
ctx.message.from.id
then we checked if this id is already present in our memory, if it doesn't that means the else
block will get executed, where we initiate the player and send the first question to the user.
The keyboard
variable defines the option buttons for the quiz, which you can see in the image below.
There are many kinds of Buttons that you can see on Telegraf docs, but I only needed callbackButton
because using this I can get to know which button the user clicked.
But if that condition were true, that is, if a user has already registered. This I check if the player has completed the game in which case I just replied the score with a link.
In telegraf, you can define custom actions using callbackButton
the one you saw above.
Markup.callbackButton(questions[0].o1,"o1")
The second parameter is the one that defines the name of the action.
bot.action('o1', async(ctx) => {
let userId = ctx.callbackQuery.message.chat.id;
updateScore(userId, 'o1')
if (players[userId].last_question >= questions.length){
quiz_end_message = `You've completed the trivia ❤️❤️❤️, you scored ${players[userId].score} out of ${questions.length}.`
const keyboard = Markup.inlineKeyboard([
Markup.urlButton('Click here to get the Prizes', 'https://idiomaticprogrammers.com'),
])
ctx.reply(quiz_end_message, Extra.markup(keyboard))
} else {
const keyboard = Markup.inlineKeyboard([
[Markup.callbackButton(questions[players[userId]['last_question']].o1,"o1")],
[Markup.callbackButton(questions[players[userId]['last_question']].o2,"o2")],
[Markup.callbackButton(questions[players[userId]['last_question']].o3,"o3")]
])
ctx.reply(questions[players[userId]['last_question']].question, Extra.markup(keyboard))
}
ctx.deleteMessage()
})
This is how we create a custom action, this callback function is called when a user clicks option 1 or the first button. I didn't wanted to handle multiple questions on a screen which led to a possibility that a user might click on a option for previous question. So, I just deleted the question once it's score is checked and updated the score.
function updateScore(playerId ,option){
const last_question = players[playerId].last_question;
console.log(players)
players[playerId]['last_question']++;
if (questions[last_question].answer === option){
players[playerId].score++;
}
}
With each click of the options, I am also checking if the user have completed the quiz, in which case I sent them the score and a link.
So that is it, it took me about 50 mins to build this and here is the entire code.
const { Telegraf } = require("telegraf")
const Extra = require('telegraf/extra')
const Markup = require('telegraf/markup')
const fs = require('fs')
const bot = new Telegraf(process.env.BOT_TOKEN)
bot.start((ctx)=>{
ctx.reply(`Hello, Are you ready to start the quiz?\nThere will be ${questions.length} question, each scoring 1 points.`)
const logo = fs.readFileSync('img/logo.jpg')
ctx.telegram.sendPhoto(ctx.chat.id, {
source: logo
}).catch(reason => {
console.log(reason)
})
})
let players = {}
let questions = [
{
"question": "The tree sends down roots from its branches to the soil is know as:",
"o1": "Oak",
"o2": "Pine",
"o3": "Banyan",
"answer": "o3"
},
{
"question": "Electric bulb filament is made of ",
"o1": "Copper",
"o2": "Lead",
"o3": "Tungsten",
"answer": "o3"
},
{
"question": "Which of the following is used in pencils?",
"o1": "Graphite",
"o2": "Lead",
"o3": "Silicon",
"answer": "o1"
},
{
"question": "The speaker of the Lok Sabha can ask a member of the house to stop speaking and let another member speak. This phenomenon is known as :",
"o1": "Crossing the floor",
"o2": "Yielding the floor",
"o3": "Calling Attention Motion",
"answer": "o2"
},
{
"question": "The Comptroller and Auditor General of India can be removed from office in like manner and on like grounds as :",
"o1": "High Court Judge",
"o2": "Prime Minister",
"o3": "Supreme Court Judge",
"answer": "o3"
},
]
bot.on('message', (ctx) => {
let userId = ctx.message.from.id;
if (userId in players){
if (players[userId]['last_question'] >= questions.length){
quiz_end_message = `You've completed the trivia 🥳 🥳, you scored ${players[userId].score} out of ${questions.length}.`
const keyboard = Markup.inlineKeyboard([
Markup.urlButton('Click here to get the Prizes', 'https://idiomaticprogrammers.com/'),
])
ctx.reply(quiz_end_message, Extra.markup(keyboard))
} else {
const keyboard = Markup.inlineKeyboard([
[Markup.callbackButton(questions[players[userId]['last_question']].o1,"o1")],
[Markup.callbackButton(questions[players[userId]['last_question']].o2,"o2")],
[Markup.callbackButton(questions[players[userId]['last_question']].o3,"o3")]
])
ctx.reply(questions[players[userId]['last_question']].question, Extra.markup(keyboard))
}
} else {
players[userId] = {
"last_question": 0,
"last_answer_time": new Date().getTime(),
"score": 0
}
console.log(userId)
const keyboard = Markup.inlineKeyboard([
[Markup.callbackButton(questions[0].o1,"o1")],
[Markup.callbackButton(questions[0].o2,"o2")],
[Markup.callbackButton(questions[0].o3,"o3")]
])
ctx.reply(questions[0].question, Extra.markup(keyboard))
}
})
function updateScore(playerId ,option){
const last_question = players[playerId].last_question;
console.log(players)
players[playerId]['last_question']++;
if (questions[last_question].answer === option){
players[playerId].score++;
}
}
bot.action('o1', async(ctx) => {
let userId = ctx.callbackQuery.message.chat.id;
updateScore(userId, 'o1')
if (players[userId].last_question >= questions.length){
quiz_end_message = `You've completed the trivia 🥳 🥳, you scored ${players[userId].score} out of ${questions.length}.`
const keyboard = Markup.inlineKeyboard([
Markup.urlButton('Click here to get the Prizes', 'https://idiomaticprogrammers.com/'),
])
ctx.reply(quiz_end_message, Extra.markup(keyboard))
} else {
const keyboard = Markup.inlineKeyboard([
[Markup.callbackButton(questions[players[userId]['last_question']].o1,"o1")],
[Markup.callbackButton(questions[players[userId]['last_question']].o2,"o2")],
[Markup.callbackButton(questions[players[userId]['last_question']].o3,"o3")]
])
ctx.reply(questions[players[userId]['last_question']].question, Extra.markup(keyboard))
}
ctx.deleteMessage()
})
bot.action('o2', async(ctx) => {
let userId = ctx.callbackQuery.message.chat.id;
updateScore(userId, 'o2')
if (players[userId].last_question >= questions.length){
quiz_end_message = `You completed the trivia, you scored ${players[userId].score} out of ${questions.length}.`
const keyboard = Markup.inlineKeyboard([
Markup.urlButton('Click here to get the Prizes', 'https://idiomaticprogrammers.com/'),
])
ctx.reply(quiz_end_message, Extra.markup(keyboard))
} else {
const keyboard = Markup.inlineKeyboard([
[Markup.callbackButton(questions[players[userId]['last_question']].o1,"o1")],
[Markup.callbackButton(questions[players[userId]['last_question']].o2,"o2")],
[Markup.callbackButton(questions[players[userId]['last_question']].o3,"o3")]
])
ctx.reply(questions[players[userId]['last_question']].question, Extra.markup(keyboard))
}
ctx.deleteMessage()
})
bot.action('o3', async(ctx) => {
let userId = ctx.callbackQuery.message.chat.id;
updateScore(userId, 'o3')
if (players[userId].last_question >= questions.length){
quiz_end_message = `You completed the trivia, you scored ${players[userId].score} out of ${questions.length}.`
const keyboard = Markup.inlineKeyboard([
Markup.urlButton('Click here to get the Prizes', 'https://idiomaticprogrammers.com/'),
])
ctx.reply(quiz_end_message, Extra.markup(keyboard))
} else {
const keyboard = Markup.inlineKeyboard([
[Markup.callbackButton(questions[players[userId]['last_question']].o1,"o1")],
[Markup.callbackButton(questions[players[userId]['last_question']].o2,"o2")],
[Markup.callbackButton(questions[players[userId]['last_question']].o3,"o3")]
])
ctx.reply(questions[players[userId]['last_question']].question, Extra.markup(keyboard))
}
ctx.deleteMessage()
})
bot.launch()
That is what I learned today.