Let's keep coding more bots.
Mar 23, 2016 · 9 minute read · Commentstelegrambotsgo
All posts of the serie.
- Gentle introduction to Telegram Bots.
- Bot Revolution. Know your API or die hard.
- Start coding some simple bots.
- 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…
- In-memory “database”, with questions and answers
- Handler to send a question
- Handler to receive the answer
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
.
- Choose the random number:
r := rand.Intn(len(questions))
choosen := questions[r]
- Function to transform a list of strings into a list of list with at max two strings in the inner lists
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
}
- Build the keyboard (selective false and resize false)
keyl := buildKeyboard(choosen.Answers)
rkm := tgbot.ReplyKeyboardMarkup{
Keyboard: keyl,
ResizeKeyboard: false,
OneTimeKeyboard: true,
Selective: false,
}
- Send it!
bot.Answer(msg).Text(choosen.Question).ReplyToMessage(msg.ID).Keyboard(rkm).End()
- Sum it up!
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
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
- get
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)
- set
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()
}
- del
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 thoughts
You can see the full code in my repository.
If you want to improve the bot, here are some ideas:
- Load the questions and the token from a JSON like:
{
"token": "AABBABBABA",
"questions": [{
"question": "The question?",
"answers": ["answer1", "answer2", "answer3"],
"solution": 1
}, {
"question": "The question 2?",
"answers": ["answer4", "answer5", "answer6"],
"solution": 0
}]
}
Check that the text in the answer is not a command
On the start of the program, check that all the solutions are in the range of the answers
Allow the admin to add questions with some command/s (hint: Add it at the end of the questions, so you don’t have to worry about “users already answering”)
Save success and fail numbers for every chat, and provide some stats, ranking, ….
Use some API to get trivia questions. Like this
If you want to learn more about concurrency, you can build the data synchronization using a goroutine and a channel where you wait for Read, Set or Delete actions. This approach are similar to what I made in the downloader bot to dispatch works using a Queue (this code are much more complex that what you should write, because the downloader code are to dispatch works, not to “do” the actualy work). To learn more about concurrency patterns in Go, check this great blog post or search in google, there are great articles and videos.
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