Writing a Telegram Bot in R (Part 4): Building a Sequential, Logical Dialogue with the Bot

If you are already familiar with the previous three articles in this series, you already know how to create fully functional Telegram bots with a keyboard.

In this article, we will learn to write a bot that will support a sequential dialogue. That is, the bot will ask you questions and wait for you to provide some information. Depending on the data you enter, the bot will perform certain actions.

We will also learn to use a database under the hood of the bot; in our example, this will be SQLite, but you can use any other DBMS. I wrote more about interacting with databases in R in from one of the authors..

Writing a Telegram Bot in R (Part 4): Building a Sequential, Logical Dialogue with the Bot

All articles from the "Writing a Telegram Bot in R" series

  1. Creating a bot and sending messages through it on Telegram
  2. Adding command support and message filters to the bot
  3. How to add keyboard support to the bot
  4. Building a Sequential, Logical Dialogue with the Bot

Content

If you're interested in data analysis, you might find my telegram and youtube channels interesting. Most of their content is devoted to the R language.

  1. Introduction
  2. The Process of Building a Bot
  3. Bot Project Structure
  4. Bot Configuration
  5. Creating an Environment Variable
  6. Creating a Database
  7. Writing Functions to Work with the Database
  8. Bot Methods
  9. Message Filters
  10. Handlers
  11. Bot Launch Code
  12. Conclusion

Introduction

For the bot to request data from you and wait for you to enter information, you will need to keep track of the current state of the dialogue. The best way to do this is to use some embedded database, like SQLite.

The logic will be as follows. We call the bot method, and the bot sequentially requests certain information from us, waiting for the input at each step, during which it can also perform validation checks.

We will write a very simple bot that first asks for your name, then your age, and saves the obtained data in the database. When asking for age, it will check to ensure that the input is a number, not text.

Such a simple dialogue will have only three states:

  1. start — the normal state of the bot in which it does not expect any information from you
  2. wait_name — the state during which the bot is waiting for you to enter your name
  3. wait_age — the state during which the bot is waiting for you to enter your age in complete years.

The Process of Building a Bot

In this article, we will build a bot step by step; the entire process can be schematically represented as follows:
Writing a Telegram Bot in R (Part 4): Building a Sequential, Logical Dialogue with the Bot

  1. We create the bot's configuration, where we will store some settings. In our case, the bot token and the path to the database file.
  2. We create an environment variable that will store the path to the project with the bot.
  3. We create the database itself and a number of functions that allow the bot to interact with it.
  4. We write the bot methods, i.e., functions that it will perform.
  5. We add message filters. These will allow the bot to call the necessary methods depending on the current state of the chat.
  6. We add handlers that will link commands and messages with the necessary bot methods.
  7. We start the bot.

Bot Project Structure

For convenience, we will break our bot code and other related files into the following structure.

  • bot.R — main code of our bot
  • db_bot_function.R — block of code with functions for database operations
  • bot_methods.R — code for the bot's methods
  • message_filters.R — message filters
  • handlers.R — handlers
  • config.cfg — bot configuration
  • create_db_data.sql — SQL script to create the chat data table in the database
  • create_db_state.sql — SQL script to create the current chat state table in the database
  • bot.db — bot's database

You can view the entire bot project, or download from my repository on GitHub.

Bot Configuration

We will use a regular ini file, of the following form:

[bot_settings]
bot_token=YOUR_BOT_TOKEN

[db_settings]
db_path=C:/PATH/TO/PROJECT/FOLDER/bot.db

In the config, we write the bot token and the database path, i.e., to the bot.db file, which we will create in the next step.

For more complex bots, you can create more sophisticated configs, and it's not necessary to write specifically ini config; you can use any other format, including JSON.

Creating an Environment Variable

On each PC, the folder with the bot project may be located in different directories and on different disks, so the path to the project folder in the code will be specified through the environment variable TG_BOT_PATH.

You can create an environment variable in several ways, the simplest is to write it in the file .Renviron.

You can create or edit this file using the command file.edit(path.expand(file.path("~", ".Renviron"))). Execute it and add a line to the file:

TG_BOT_PATH=C:/PATH/TO/YOUR/PROJECT

Then save the file .Renviron and restart RStudio.

Creating a Database

The next step is to create the database. We will need 2 tables:

  • chat_data — data requested by the bot from the user
  • chat_state — the 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 downloaded the bot project from GitHub, you can use the following R code to create the database.

# Скрипт создания базы данных
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 to Work with the Database

We already have a configuration file and a database created. Now we need to write functions for reading and writing data to this database.

If you downloaded the project from GitHub, you can find the functions in the file db_bot_function.R.

Code for functions to work 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 created 4 simple functions:

  • get_state() — retrieve the current state of the chat from the database
  • set_state() — write the current state of the chat to the database
  • get_chat_data() — retrieve data sent by the user
  • set_chat_data() — write data received from the user

All functions are quite simple; they either read data from the database using the command dbGetQuery(), or perform UPSERT operations (modifying existing data or writing new data to the database), using the function dbExecute().

The syntax for the UPSERT operation looks as follows:

INSERT INTO chat_data (chat_id, ${field})
VALUES(${chat_id}, '${value}') 
ON CONFLICT(chat_id) 
DO UPDATE SET ${field}='${value}';

That is, in our tables, the field chat_id has a uniqueness constraint and is the primary key of the tables. Initially, we try to add information to the table and receive an error if data for the current chat already exists; in such a case, we simply update the information for this chat.

We will use these functions in the methods and filters of the bot.

Bot Methods

The next step in building our bot will be creating methods. If you downloaded the project from GitHub, all methods are located in the file bot_methods.R.

Code for the bot's methods

# ###########################################################
# 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 created 5 methods:

  • start — Start the dialog
  • state — Get the current state of the chat
  • reset — Reset the current state of the chat
  • enter_name — The bot asks for your name
  • enter_age — The bot asks for your age

Element.getAnimations() start asks for your name and changes the state of the chat to wait_name, i.e., in the waiting mode for your name input.

Then, you send your name, and it is processed by the method enter_name, the bot greets you, records the received name in the database, and changes the chat state to wait_age.

At this stage, the bot is waiting for you to enter your age. You send your age, and the bot checks the message; if you sent something other than a number, it will say: You entered invalid data, please enter a number, and will wait for you to enter the data again. If you sent a number, the bot will confirm that it has accepted your age, will store the received data in the database, will inform you of all the data received from you, and will reset the chat to its initial state, i.e., start.

By calling the method state you can request the current state of the chat at any time, and by the method reset reset the chat to its initial state.

Message Filters

In our case, this is one of the most important parts of building the bot. It is through message filters that the bot will understand what information it is waiting for from you and how to process it.

In the project on GitHub the filters are defined in the file message_filters.R.

Message filter 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 the filters, we use the function written previously get_state(), to request the current state of the chat. This function requires just one argument, the chat ID.

Next, the filter wait_name processes messages when the chat is in the state wait_name, and correspondingly the filter wait_age processes messages when the chat is in the state wait_age.

Handlers

The file with handlers 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)

First, we create command handlers that will allow you to launch methods to start the dialog, reset it, and request the current state.

Next, we create 2 message handlers using the filters created in the previous step, and add the filter !MessageFilters$command, so that we can use commands in any chat state.

Bot Launch Code

Now we are all set to launch; the main bot startup code is in the file bot.R.

library(telegram.bot)
library(tidyverse)
library(RSQLite)
library(DBI)
library(configr)

# enter the project folder
setwd(Sys.getenv('TG_BOT_PATH'))

# read the config
cfg <- read.config('config.cfg')

# create an instance of the bot
updater <- Updater(cfg$bot_settings$bot_token)

# Loading bot components
source('db_bot_function.R') # functions for working with the DB
source('bot_methods.R')     # bot methods
source('message_filters.R') # message filters
source('handlers.R') # message handlers

# Add handlers to the dispatcher
updater <- updater +
  start_h +
  wait_age_h +
  wait_name_h +
  state_h +
  reset_h

# Start the bot
updater$start_polling()

As a result, we ended up with the following bot:
Writing a Telegram Bot in R (Part 4): Building a Sequential, Logical Dialogue with the Bot

At any moment, using the command /state we can request the current state of the chat, and using the command /reset reset the chat to its original state and start the conversation anew.

Conclusion

In this article, we explored how to use databases within the bot and how to build sequential logical dialogues by capturing the chat state.

In this case, we examined the most primitive example to make it easier for you to understand the idea of building such bots; in practice, you can create much more complex dialogues.

In the next article in this series, we will learn how to restrict bot users' access to various methods.

Source: habr.com

Buy reliable website hosting with DDoS protection, VPS VDS servers 🔥 Buy reliable website hosting with DDoS protection, VPS VDS servers | ProHoster