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.

Writing a telegram bot in R (part 4): Building a consistent, logical dialogue with the bot

All articles from the series "Writing a telegram bot in R"

  1. Create a bot and use it to send messages to telegram
  2. Add command support and message filters to the bot
  3. How to add keyboard support to a bot
  4. Building a consistent, logical dialogue with the bot

Content

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.

  1. Introduction
  2. Bot building process
  3. Bot project structure
  4. Bot config
  5. Create an environment variable
  6. Create a database
  7. Writing functions for working with the database
  8. Bot Methods
  9. Message filters
  10. Handlers
  11. Bot launch code
  12. Conclusion

Introduction

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:

  1. start - the normal state of the bot, in which it does not expect any information from you
  2. wait_name - the state in which the bot is waiting for a name to be entered
  3. 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:
Writing a telegram bot in R (part 4): Building a consistent, logical dialogue with the bot

  1. 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.
  2. 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 so that the bot can interact with it.
  4. We write bot methods, i.e. the functions it will perform.
  5. Adding message filters. With the help of which the bot will access the necessary methods, depending on the current state of the chat.
  6. We add handlers that will associate commands and messages with the necessary bot methods.
  7. 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
  • bot.db - bot database

The entire bot project can be viewed, or download from my repository on GitHub.

Bot config

As a config, we will use the usual ini file, of the following form:

[bot_settings]
bot_token=ТОКЕН_ВАШЕГО_БОТА

[db_settings]
db_path=C:/ПУТЬ/К/ПАПКЕ/ПРОЕКТА/bot.db

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:
Writing a telegram bot in R (part 4): Building a consistent, logical dialogue with the 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.

Source: habr.com

Add a comment