# Telegram Trivia Bot

`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](https://www.npmjs.com/package/telegraf).

Then I quickly built a fresh folder in my Projects directory and initiated npm.

```bash
npm init -y
```

After that, I installed telegraf (obviously)

```bash
npm i telegraf
```

Copy pasted the example code provided by Telegraf and realised there is a problem.

```javascript
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](https://t.me/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

```bash
node index.js
```

Searched my BOT on Telegram and ...

![Telegram Chat](https://cdn.hashnode.com/res/hashnode/image/upload/v1741074527605/e0e91df4-b591-40e1-a837-2cb6d4c649cd.png)

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.

```javascript
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"
    }
]
```

```javascript
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.

```javascript
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.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1741074529501/647161ee-006c-4295-baeb-0e44320ad1ae.png)

> 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.

```javascript
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

```javascript
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.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1741074531220/710b40dd-3fec-4737-a12c-3b7292e5e92d.png)

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.

```javascript
Markup.callbackButton(questions[0].o1,"o1")
```

The second parameter is the one that defines the name of the action.

```javascript
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.

```javascript
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.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1741074532285/9176a349-a18d-403f-8bdf-05158c8ff090.png)

So that is it, it took me about 50 mins to build this and here is the entire code.

```javascript
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.

