ProHoster > Blog > Administration > Writing a telegram bot in R (part 4): Building a consistent, logical dialogue with the bot
Writing a telegram bot in R (part 4): Building a consistent, logical dialogue with the bot
If you have already read the previous three articles from this series, then you already know how to write full-fledged telegram bots with a keyboard.
In this article, we will learn how to write a bot that will maintain a consistent dialogue. Those. the bot will ask you questions and wait for you to enter some information. Depending on the data you entered, the bot will perform some actions.
Also in this article we will learn how to use a database bot under the hood, in our example it will be SQLite, but you can use any other DBMS. I wrote more about interacting with databases in the R language in this article.
All articles from the series "Writing a telegram bot in R"
If you are interested in data analysis, you might be interested in my telegram и youtube channels. Most of the content of which is devoted to the R language.
In order for the bot to be able to request data from you and wait for the input of any information, you will need to fix the current state of the dialog. The best way to do this is to use some kind of embedded database like SQLite.
Those. the logic will be as follows. We call the bot method, and the bot sequentially asks us for some information, while at each step it waits for this information to be entered, and can check it.
We will write the most simple bot, first it will ask for your name, then your age, the received data will be saved to the database. When asking for age, it will check that the entered data is a number and not text.
Such a simple dialog will have only three states:
start - the normal state of the bot, in which it does not expect any information from you
wait_name - the state in which the bot is waiting for a name to be entered
wait_age - the state in which the bot is waiting for your age to be entered, the number of full years.
Bot building process
In the course of the article, we will build a bot step by step, the whole process can be schematically depicted as follows:
We create a bot config in which we will store some settings. In our case, the bot token, and the path to the database file.
Create an environment variable that will store the path to the project with the bot.
We create the database itself, and a number of functions so that the bot can interact with it.
We write bot methods, i.e. the functions it will perform.
Adding message filters. With the help of which the bot will access the necessary methods, depending on the current state of the chat.
We add handlers that will associate commands and messages with the necessary bot methods.
Let's start the bot.
Bot project structure
For convenience, we will split the code of our bot, and other related files into the following structure.
bot.R - the main code of our bot
db_bot_function.R - block of code with functions for working with the database
bot_methods.R — bot methods code
message_filters.R - message filters
handlers.R - handlers
config.cfg - bot config
create_db_data.sql - SQL script for creating a table with chat data in the database
create_db_state.sql - SQL script for creating a table of the current state of the chat in the database
In the config, we write the bot token, and the path to the database, i.e. to the bot.db file, we will create the file itself in the next step.
For more complex bots, you can create more complex configs, besides, it is not necessary to write the ini config, you can use any other format, including JSON.
Create an environment variable
On each PC, the folder with the bot project can be located in different directories and on different drives, so in the code the path to the project folder will be set through the environment variable TG_BOT_PATH.
You can create an environment variable in several ways, the easiest way is to write it in a file .Renviron.
You can create or edit this file using the command file.edit(path.expand(file.path("~", ".Renviron"))). Run it and add one line to the file:
TG_BOT_PATH=C:/ПУТЬ/К/ВАШЕМУ/ПРОЕКТУ
Next, save the file .Renviron and restart RStudio.
Create a database
The next step is to create a database. We need 2 tables:
chat_data - data that the bot requested from the user
chat_state - current state of all chats
You can create these tables using the following SQL query:
CREATE TABLE chat_data (
chat_id BIGINT PRIMARY KEY
UNIQUE,
name TEXT,
age INTEGER
);
CREATE TABLE chat_state (
chat_id BIGINT PRIMARY KEY
UNIQUE,
state TEXT
);
If you have downloaded the bot project from GitHub, then you can use the following R code to create the base.
# Скрипт создания базы данных
library(DBI) # интерфейс для работы с СУБД
library(configr) # чтение конфига
library(readr) # чтение текстовых SQL файлов
library(RSQLite) # драйвер для подключения к SQLite
# директория проекта
setwd(Sys.getenv('TG_BOT_PATH'))
# чтение конфига
cfg <- read.config('config.cfg')
# подключение к SQLite
con <- dbConnect(SQLite(), cfg$db_settings$db_path)
# Создание таблиц в базе
dbExecute(con, statement = read_file('create_db_data.sql'))
dbExecute(con, statement = read_file('create_db_state.sql'))
Writing functions for working with the database
We already have a configuration file ready and a database created. Now we need to write functions for reading and writing data to this database.
If you have downloaded the project from GitHub, then you can find the functions in the file db_bot_function.R.
Code of functions for working with the database
# ###########################################################
# Function for work bot with database
# получить текущее состояние чата
get_state <- function(chat_id) {
con <- dbConnect(SQLite(), cfg$db_settings$db_path)
chat_state <- dbGetQuery(con, str_interp("SELECT state FROM chat_state WHERE chat_id == ${chat_id}"))$state
return(unlist(chat_state))
dbDisconnect(con)
}
# установить текущее состояние чата
set_state <- function(chat_id, state) {
con <- dbConnect(SQLite(), cfg$db_settings$db_path)
# upsert состояние чата
dbExecute(con,
str_interp("
INSERT INTO chat_state (chat_id, state)
VALUES(${chat_id}, '${state}')
ON CONFLICT(chat_id)
DO UPDATE SET state='${state}';
")
)
dbDisconnect(con)
}
# запись полученных данных в базу
set_chat_data <- function(chat_id, field, value) {
con <- dbConnect(SQLite(), cfg$db_settings$db_path)
# upsert состояние чата
dbExecute(con,
str_interp("
INSERT INTO chat_data (chat_id, ${field})
VALUES(${chat_id}, '${value}')
ON CONFLICT(chat_id)
DO UPDATE SET ${field}='${value}';
")
)
dbDisconnect(con)
}
# read chat data
get_chat_data <- function(chat_id, field) {
con <- dbConnect(SQLite(), cfg$db_settings$db_path)
# upsert состояние чата
data <- dbGetQuery(con,
str_interp("
SELECT ${field}
FROM chat_data
WHERE chat_id = ${chat_id};
")
)
dbDisconnect(con)
return(data[[field]])
}
We have created 4 simple functions:
get_state() — get the current state of the chat from the database
set_state() — write the current state of the chat to the database
get_chat_data() - get data sent by the user
set_chat_data() — record the data received from the user
All functions are quite simple, they either read data from the database using the command dbGetQuery(), or make UPSERT operation (changing existing data or writing new data to the database), using the function dbExecute().
The syntax of the UPSERT operation is as follows:
INSERT INTO chat_data (chat_id, ${field})
VALUES(${chat_id}, '${value}')
ON CONFLICT(chat_id)
DO UPDATE SET ${field}='${value}';
Those. field in our tables chat_id has a unique constraint and is the primary key of the tables. Initially, we try to add information to the table, and we get an error if the data for the current chat is already present, in which case we simply update the information for this chat.
Further, we will use these functions in the methods and filters of the bot.
Bot Methods
The next step in building our bot is to create methods. If you have downloaded the project from GitHub, then all methods are in the file bot_methods.R.
Bot methods code
# ###########################################################
# bot methods
# start dialog
start <- function(bot, update) {
#
# Send query
bot$sendMessage(update$message$chat_id,
text = "Введи своё имя")
# переключаем состояние диалога в режим ожидания ввода имени
set_state(chat_id = update$message$chat_id, state = 'wait_name')
}
# get current chat state
state <- function(bot, update) {
chat_state <- get_state(update$message$chat_id)
# Send state
bot$sendMessage(update$message$chat_id,
text = unlist(chat_state))
}
# reset dialog state
reset <- function(bot, update) {
set_state(chat_id = update$message$chat_id, state = 'start')
}
# enter username
enter_name <- function(bot, update) {
uname <- update$message$text
# Send message with name
bot$sendMessage(update$message$chat_id,
text = paste0(uname, ", приятно познакомится, я бот!"))
# Записываем имя в глобальную переменную
#username <<- uname
set_chat_data(update$message$chat_id, 'name', uname)
# Справшиваем возраст
bot$sendMessage(update$message$chat_id,
text = "Сколько тебе лет?")
# Меняем состояние на ожидание ввода имени
set_state(chat_id = update$message$chat_id, state = 'wait_age')
}
# enter user age
enter_age <- function(bot, update) {
uage <- as.numeric(update$message$text)
# проверяем было введено число или нет
if ( is.na(uage) ) {
# если введено не число то переспрашиваем возраст
bot$sendMessage(update$message$chat_id,
text = "Ты ввёл некорректные данные, введи число")
} else {
# если введено число сообщаем что возраст принят
bot$sendMessage(update$message$chat_id,
text = "ОК, возраст принят")
# записываем глобальную переменную с возрастом
#userage <<- uage
set_chat_data(update$message$chat_id, 'age', uage)
# сообщаем какие данные были собраны
username <- get_chat_data(update$message$chat_id, 'name')
userage <- get_chat_data(update$message$chat_id, 'age')
bot$sendMessage(update$message$chat_id,
text = paste0("Тебя зовут ", username, " и тебе ", userage, " лет. Будем знакомы"))
# возвращаем диалог в исходное состояние
set_state(chat_id = update$message$chat_id, state = 'start')
}
}
We have created 5 methods:
start — Starting a dialog
state — Get the current state of the chat
reset - Reset the current chat state
enter_name - The bot is asking for your name
enter_age - Bot asks for your age
Method start asks for your name, and changes the chat state to wait_name, i.e. to enter your name.
Next, you send a name and it is processed by the method enter_name, the bot greets you, writes the received name to the database, and switches the chat to the state wait_age.
At this stage, the bot is waiting for you to enter your age. You send your age, the bot checks the message, if you sent some text instead of a number, it will say: Ты ввёл некорректные данные, введи число, and will wait for you to re-enter data. If you sent a number, the bot will report that it has accepted your age, write the received data to the database, report all the data received from you and transfer the chat state to its original position, i.e. V start.
By calling the method state you can request the current state of the chat at any time, and using the reset reset the chat.
Message filters
In our case, this is one of the most important parts in building a bot. It is with the help of message filters that the bot will understand what information it expects from you, and how it should be processed.
In the project for GitHub filters are written in the file message_filters.R.
Message filters code:
# ###########################################################
# message state filters
# фильтр сообщений в состоянии ожидания имени
MessageFilters$wait_name <- BaseFilter(function(message) {
get_state( message$chat_id ) == "wait_name"
}
)
# фильтр сообщений в состоянии ожидания возраста
MessageFilters$wait_age <- BaseFilter(function(message) {
get_state( message$chat_id ) == "wait_age"
}
)
In filters, we use the previously written function get_state(), in order to query the current state of the chat. This function requires only 1 argument, chat id.
Next filter wait_name processes messages when the chat is in state wait_name, and, accordingly, the filter wait_age processes messages when the chat is in state wait_age.
Handlers
The handler file is called handlers.R, and has the following code:
# ###########################################################
# handlers
# command handlers
start_h <- CommandHandler('start', start)
state_h <- CommandHandler('state', state)
reset_h <- CommandHandler('reset', reset)
# message handlers
## !MessageFilters$command - означает что команды данные обработчики не обрабатывают,
## только текстовые сообщения
wait_age_h <- MessageHandler(enter_age, MessageFilters$wait_age & !MessageFilters$command)
wait_name_h <- MessageHandler(enter_name, MessageFilters$wait_name & !MessageFilters$command)
We first create command handlers that will allow you to run methods to start a dialog, reset it, and query the current state.
Next, we create 2 message handlers using the filters created in the previous step, and add a filter to them !MessageFilters$command, so that we can use commands in any chat state.
Bot launch code
Now we are ready to launch, the main code for launching the bot is in the file bot.R.
library(telegram.bot)
library(tidyverse)
library(RSQLite)
library(DBI)
library(configr)
# переходим в папку проекта
setwd(Sys.getenv('TG_BOT_PATH'))
# читаем конфиг
cfg <- read.config('config.cfg')
# создаём экземпляр бота
updater <- Updater(cfg$bot_settings$bot_token)
# Загрузка компонентов бота
source('db_bot_function.R') # функции для работы с БД
source('bot_methods.R') # методы бота
source('message_filters.R') # фильтры сообщений
source('handlers.R') # обработчики сообщений
# Добавляем обработчики в диспетчер
updater <- updater +
start_h +
wait_age_h +
wait_name_h +
state_h +
reset_h
# Запускаем бота
updater$start_polling()
As a result, we got the following bot:
At any time with the command /state we can query the current state of the chat, and using the command /reset reset the chat to its original state and start the dialogue again.
Conclusion
In this article, we figured out how to use databases inside the bot, and how to build sequential logical dialogues by fixing the chat state.
In this case, we have considered the most primitive example, in order to make it easier for you to understand the idea of building such bots, in practice you can build much more complex dialogs.
In the next article in this series, we will learn how to restrict bot users' rights to use various bot methods.