Let's keep coding more bots.


All posts of the serie.

  1. Gentle introduction to Telegram Bots.
  2. Bot Revolution. Know your API or die hard.
  3. Start coding some simple bots.
  4. Let's keep coding more bots.

Hello Friend, it’s been a long time without writing, but here we are, coding some more bots :)

The other day some one talked to me in Telegram asking me to write some more posts with more complex bots, and… well, let’s program! =D

The idea

First of all, we need the idea to build the bot, without it, we won’t be able to write anything.

Let’s start with some intermediate-idea, just some ask-answer bot, without database or API connections to get started in this world.

And a really simple ask-answer bot is a trivia bot.

Break it in parts

So, let’s think about the idea.

The bot will have some questions with the answers and the solution, this will be loaded at startup or hard-coded in the code. When the user asks for a question, the bot will get a random one, and send it, with a keyboard with the possible answers, and replying to the message. And finally, when the user answers, we should check if it’s the correct one, and answer if it’s right or wrong.

So…

With this we have some parts to start coding, let’s go!

The Data are your friend

To save and build the questions we are going to use a struct, for example:

type question struct {
    Question string   `json:"question"`
    Answers  []string `json:"answers"`
    Solution int      `json:"solution"`
}

Here we are defining a struct question, with the “q” in lowercase, so it’s not an exported struct, that have three fields, a Question that is an string, some Answers that is a list of strings, and a Solution, that is just the position in the answers where the good answer is (starting in zero, because we are programmers).

Let’s define a first “database”:

var questions = []question{
    question{"Current year?", []string{"1994", "2015", "2016", "2017"}, 2},
}

A simple question, with some answers, and the index of the good answer.

I have some questions…

Before we code the function to send the question/answers, we need to build the bot and the handler, as we saw in the previous post, this is really easy:

func main() {
    token := ""

    bot := tgbot.New(token).
        SimpleCommandFn(`question`, questionHandler)

    bot.SimpleStart()
}

Just the main function, with a token variable, we create the bot with the token, and set the handler with a Simple Command, so that handler will be executed with /question and /question@usernamebot

To start the handler, because this is a Simple Command, the initial function is like this:

func questionHandler(bot tgbot.TgBot, msg tgbot.Message, text string) *string {

    return nil
}

What do we need to do? First, choose a random question, then build the keyboard, and at last send the question with the keyboard.

To choose a random question we are going to use rand.Intn, to build the keyboard, that are of type [][]string, we will define a function to transform a []string into that with two strings top, and then build the tgbot.ReplyKeyboardMarkup.

r := rand.Intn(len(questions))
choosen := questions[r]
func buildKeyboard(ops []string) [][]string {
    keylayout := [][]string{{}}
    for _, k := range ops {
        if len(keylayout[len(keylayout)-1]) == 2 {
            keylayout = append(keylayout, []string{k})
        } else {
            keylayout[len(keylayout)-1] = append(keylayout[len(keylayout)-1], k)
        }
    }
    return keylayout
}
keyl := buildKeyboard(choosen.Answers)
rkm := tgbot.ReplyKeyboardMarkup{
    Keyboard:        keyl,
    ResizeKeyboard:  false,
    OneTimeKeyboard: true,
    Selective:       false,
}
bot.Answer(msg).Text(choosen.Question).ReplyToMessage(msg.ID).Keyboard(rkm).End()
func questionHandler(bot tgbot.TgBot, msg tgbot.Message, text string) *string {
    r := rand.Intn(len(questions))
    choosen := questions[r]

    keyl := buildKeyboard(choosen.Answers)
    rkm := tgbot.ReplyKeyboardMarkup{
        Keyboard:        keyl,
        ResizeKeyboard:  false,
        OneTimeKeyboard: true,
        Selective:       false,
    }

    bot.Answer(msg).Text(choosen.Question).ReplyToMessage(msg.ID).Keyboard(rkm).End()

    return nil
}

We can test how it’s going the bot!

If you named the file main.go and the token are setted in the variable, just execute go run main.go

Question image

Ok, that wasn’t that hard =D

concurrency. was At beginning the everything

At this point, our user will receive the question and the answers, and we want to get, process and know if the answer is right.

And we are facing our first not expected problem… How do we know what question are the user answering to know if it’s correct? Even more, how do we even know if the user are answering any question?

We need to save, in some way, if the user are answering AND some kind of reference to the question.

Our first approach can be: Just have a global map[int]int where you save in the key the user and in the value the question number, and this is not bad at all, actually we are going to use something similar, but when you are used to built some web apps with many users using it at the same time, and your app are concurrent… You are in trouble.

But in go, for something so simple like this, we can built a really simple way to use a simple concurrent datastructure.

We are going to use sync.RWMutex, in this way the write operations will be blocking, but the read operations won’t.

Thanks to the struct extensions in Go, we can declare our data structure in this way:

type usersAnsweringStruct struct {
    *sync.RWMutex
    Users map[int]int
}

This kind of struct say that is extending *sync.RWMutex, and have a field Users.

And let’s create a global instance of this struct:

var usersAnswering = usersAnsweringStruct{&sync.RWMutex{}, make(map[int]int)}

In this way, when we want to read something we just need to do something like:

usersAnswering.RLock()
// Use usersAnswering.Users
usersAnswering.RUnlock()

BUT, in my experience, is much more easy to provide an easy to use API of the structure, so… let’s built it! We are going to need get, set and del

We are just going to RLock the instance, read, and RUnlock it, then return the results.

func (users *usersAnsweringStruct) get(user int) (int, bool) {
    users.RLock()
    i, ok := users.Users[user]
    users.RUnlock()
    return i, ok
}

If you know some Go, you can be thinking: Why call RUnlock by hand instead of using defer users.RUnlock()? Well, because calling it manually are faster, and in this simple example, we don’t need to defer the call, we don’t have weird code branches or complex calls, we are just locking, single operation and unlocking. (This will be applied to all the methods)

In the same way, just lock, set and unlock, but in this case, we use Lock and Unlock to do a write lock

func (users *usersAnsweringStruct) set(user int, value int) {
    users.Lock()
    users.Users[user] = value
    users.Unlock()
}

Just the same as set but deleting the key

func (users *usersAnsweringStruct) del(user int) {
    users.Lock()
    delete(users.Users, user)
    users.Unlock()
}

So…. We just have our beauty easy to use interface over our data structure, and we can do things like usersAnswering.del(userID) without thinking about concurrency, it’s all handled now! YAY!

Before we start coding our super answer handler, we have to save that the user are answering :)

Let’s modify the questionHandler by adding a set operation (usersAnswering.set(msg.Chat.ID, r)) before the answer:

func questionHandler(bot tgbot.TgBot, msg tgbot.Message, text string) *string {
    r := rand.Intn(len(questions))
    choosen := questions[r]

    keyl := buildKeyboard(choosen.Answers)
    rkm := tgbot.ReplyKeyboardMarkup{
        Keyboard:        keyl,
        ResizeKeyboard:  false,
        OneTimeKeyboard: true,
        Selective:       false,
    }

    usersAnswering.set(msg.Chat.ID, r)

    bot.Answer(msg).Text(choosen.Question).ReplyToMessage(msg.ID).Keyboard(rkm).End()

    return nil
}

Now we can move on!

Note: If we were using a database, someone maybe will think in saving the user information to the database and check it, but I think that it’s better in this “cache”, it’s concurrent-safe, and it’s much more faster than the database, and we don’t want to have many database operations concurrently, it’s a really slow task. (Maybe I would think in doing this if my system must be distributed in many servers, if that are the system I would use Redis to save this kind of fast-access data)

Do you have my answer?

I always love to think about the handler first, and then write it. This one is really tricky, because it will be human-text, it’s not a command, and we don’t know easily any regexp to build it, so… We are going to use a generic handler and we’ll decide inside.

We are going to use the NotCalledFn, that it will be called… well… if any of the other handlers has not been called =D

bot := tgbot.New(token).
    SimpleCommandFn(`question`, questionHandler).
    NotCalledFn(maybeAnswerHandler)

The NotCalled handler are a really generic one, let’s start, as always, with the empty function:

func maybeAnswerHandler(bot tgbot.TgBot, msg tgbot.Message) {

}

First, because this function can be called with ANY event, we want to be sure that the message have text:

if msg.Text == nil {
    return
}

text := *msg.Text

Then we want to get the user from our cache, delete from it (because once we get it once, we can safely remove from it) and check if it’s answering and the int value are on bounds.

i, ok := usersAnswering.get(msg.Chat.ID)

usersAnswering.del(msg.Chat.ID) // We can safely remove right now

if !ok || i < 0 || i >= len(questions) {
    bot.Answer(msg).Text("You need to start a /question first").End()
    return
}

Once we know that the user ARE answering and the we have the question, let’s check the answer with the solution and act.

choosen := questions[i]
goodone := choosen.Answers[choosen.Solution]

if text == goodone {
    bot.Answer(msg).Text("SUCCESS!").End()
    return
}

bot.Answer(msg).Text("WRONG!").End()

Let’s see the full function:

func maybeAnswerHandler(bot tgbot.TgBot, msg tgbot.Message) {
    if msg.Text == nil {
        return
    }

    text := *msg.Text

    i, ok := usersAnswering.get(msg.Chat.ID)

    usersAnswering.del(msg.Chat.ID) // We can safely remove right now

    if !ok || i < 0 || i >= len(questions) {
        bot.Answer(msg).Text("You need to start a /question first").End()
        return
    }

    choosen := questions[i]
    goodone := choosen.Answers[choosen.Solution]

    if text == goodone {
        bot.Answer(msg).Text("SUCCESS!").End()
        return
    }

    bot.Answer(msg).Text("WRONG!").End()
}

Let’s see how it looks ^^

Final bot

Final thoughts

You can see the full code in my repository.

If you want to improve the bot, here are some ideas:

{
    "token": "AABBABBABA",
    "questions": [{
        "question": "The question?",
        "answers": ["answer1", "answer2", "answer3"],
        "solution": 1
    }, {
        "question": "The question 2?",
        "answers": ["answer4", "answer5", "answer6"],
        "solution": 0
    }]
}

Thanks!

And that’s all!

It wasn’t that hard, didn’t it?

All you need to do now are program your own bots and let your imagination fly ;-)

Good luck, and remember that you can be in touch with be in Telegram

comments powered by Disqus