Telegram Trivia Bot

In this post, we go through the process of building a Trivia chatbot on Telegram.

Telegram Trivia Bot

I was tasked to build a trivia chatbot, 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 on 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 was 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 realized 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 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 server by

node index.js

Searched my BOT on Telegram and ...

Telegram Chat

So, 10 mins into code I have a basic chatbot ready. Since this was just a proof of concept, I didn't want to spend time setting up a 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 = {}

I had 5 questions, but for this blog, there is only one. Also, I will initiate the players dynamically later on.

On 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)
    })
})

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 developer 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 has 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 want to handle multiple questions on a screen which led to the possibility that a user might click on an option for the previous question. So, I just deleted the question once its 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 has 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.

Did you find this article valuable?

Support Idiomatic Programmers by becoming a sponsor. Any amount is appreciated!