Ако вече сте запознали с предишните от тази серия, вече можете да пишете полноценни telegram ботове с клавиатура.
В тази статия ще научим как да напишем бот, който ще поддържа последователен диалог. Т.е. ботът ще ви задава въпроси и ще изчаква от вас да въведете информация. В зависимост от въведените от вас данни, ботът ще изпълнява определени действия.
Също в тази статия ще научим как да използваме бази данни под капака на бота, в нашия пример това ще бъде SQLite, но можете да използвате и друга СУБД. По-подробно за взаимодействието с бази данни на езика R писах в .

Всички статии от серията "Пишем телеграм бот на езика R"
Съдържание
Ако се интересувате от анализ на данни, вероятно ще ви бъдат интересни моите и канали. Голямата част от съдържанието, което се отнася до езика R.
Въведение
За да може ботът да изисква данни от вас и да изчаква въвеждането на информация, е необходимо да запомните текущото състояние на диалога. Най-добрият начин да го направите е да използвате вградена база данни, например SQLite.
Т.е. логиката ще следва следното. Извикваме метод на бота, и той последователно изисква от нас информация, като на всяка стъпка изчаква въвеждането на тази информация и може да я проверява.
Ще напишем максимално прост бот, който първо ще пита за вашето име, след това за възраст, като получените данни ще се запазват в базата данни. При запитване за възрастта той ще проверява дали въведените данни са число, а не текст.
Такъв прост диалог ще има само три състояния:
- start — обичното състояние на бота, в което той не очаква от вас никаква информация
- wait_name — състояние, при което ботът очаква въвеждане на име
- wait_age — състояние, при което ботът очаква въвеждане на вашата възраст, брой завършени години.
Процесът на изграждане на бота
В хода на статията ще изградим бота стъпка по стъпка, целият процес може схематично да бъде представен по следния начин:

- Създаваме конфигурация на бота, в която ще съхранляваме някои настройки. В нашия случай токена на бота и пътя до файла на базата данни.
- Създаваме променлива за средата, в която ще се съхранява пътя до проекта с бота.
- Създаваме самата база данни и редица функции, за да може ботът да взаимодейства с нея.
- Пишем методите на бота, т.е. функциите, които той ще изпълнява.
- Добавяме филтри за съобщения. С помощта на които ботът ще се обръща към нужните методи в зависимост от текущото състояние на чата.
- Добавяме обработчици, които свързват командите и съобщенията с нужните методи на бота.
- Стартираме бота.
Структура на проекта на бота
За удобство ще разделим кода на нашия бот и другите свързани с него файлове на следната структура.
- bot.R — основният код на нашия бот
- db_bot_function.R — блок код с функции за работа с базата данни
- bot_methods.R — код на методите на бота
- message_filters.R — филтри за съобщения
- handlers.R — обработчици
- config.cfg — конфиг на бота
- create_db_data.sql — SQL скрипт за създаване на таблица с данни от чата в базата данни
- create_db_state.sql — SQL скрипт за създаване на таблица за текущото състояние на чата в базата данни
- bot.db — базата данни на бота
Целият проект на бота може да бъде видян, или от моят .
Конфигурацията на бота
Като конфиг ще използваме обикновен , следния вид:
[bot_settings]
bot_token=ТОКЕН_НА_ВАШИЯ_БОТ
[db_settings]
db_path=C:/ПЪТ/К/ПАПКАТА/НА/ПРОЕКТА/bot.dbВ конфигурацията записваме токена на бота и пътя до базата данни, т.е. до файла bot.db, самият файл ще създадем на следващата стъпка.
За по-сложни ботове може да се създават и по-сложни конфигурации, освен това не е задължително да пишете точно ini конфигурация, можете да използвате всеки друг формат, включително JSON.
Създаване на променлива за средата
На всеки ПК папката с проекта на бота може да се намира в различни директории и на различни дискове, затова в кода пътят към папката на проекта ще бъде зададен чрез променлива на средата TG_BOT_PATH.
Може да създадете променлива на средата по няколко начина, най-простият — да я запишете в файла .Renviron.
Можете да създадете или редактирате този файл с командата file.edit(path.expand(file.path("~", ".Renviron"))). Изпълнете я и добавете в файла един ред:
TG_BOT_PATH=C:/ПЪТ/К/ВАШИЯ/ПРОЕКТСлед това запишете файла .Renviron и рестартирайте RStudio.
Създаване на база данни
Следващата стъпка е създаването на базата данни. Ще ни трябват 2 таблици:
- chat_data — данните, които ботът е поискал от потребителя
- chat_state — текущото състояние на всички чати
Тези таблици могат да бъдат създадени с помощта на следния SQL запитване:
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
);
Ако сте изтеглили проекта на бота от , за да създадете база, можете да използвате следния код на езика R.
# Скрипт создания базы данных
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'))
Писане на функции за работа с базата данни
Вече имаме готов файл за конфигуриране и създадена база данни. Сега е нужно да напишем функции за четене и запис на данни в тази база.
Ако сте изтеглили проекта от , можете да намерите функциите във файла db_bot_function.R.
Код на функциите за работа с базата данни
# ###########################################################
# 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]])
}Создадохме 4 прости функции:
get_state()— да получите текущото състояние на чата от БДset_state()— да запишете текущото състояние на чата в БДget_chat_data()— да получите данни, изпратени от потребителяset_chat_data()— да запишете данни, получени от потребителя
Всички функции са доста прости, те или четат данни от базата с помощта на командата dbGetQuery(), или извършват UPSERT операция (промяна на съществуващи данни или запис на нови данни в БД), с помощта на функцията dbExecute().
Синтаксисът на UPSERT операцията изглежда по следния начин:
INSERT INTO chat_data (chat_id, ${field})
VALUES(${chat_id}, '${value}')
ON CONFLICT(chat_id)
DO UPDATE SET ${field}='${value}';Т.е. в нашите таблици полето chat_id има ограничение за уникалност и е първичен ключ на таблиците. Първоначално пробваме да добавим информация в таблицата, и получаваме грешка, ако данните за текущия чат вече присъстват, в такъв случай просто актуализираме информацията за този чат.
Следващите функции ще използваме в методите и филтрите на бота.
Методи на бота
Следващата стъпка при изграждането на нашия бот ще бъде създаването на методи. Ако сте изтеглили проекта от , всички методи се намират в файла bot_methods.R.
Код на методите на бота
# ###########################################################
# 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')
}
}Създадохме 5 метода:
- start — Стартиране на диалога
- state — Получаване на текущото състояние на чата
- reset — Нулиране на текущото състояние на чата
- enter_name — Ботът пита за вашето име
- enter_age — Ботът пита за вашата възраст
Методът стартиране пита за вашето име и превежда състоянието на чата в wait_name, т.е. в режим на изчакване за въвеждане на вашето име.
След това вие изпращате името и то се обработва чрез метода enter_name, ботът ви поздравява, записва полученото име в базата, и превежда чата в състояние wait_age.
На този етап ботът чака от вас да въведете вашата възраст. Вие изпращате вашата възраст, ботът проверява съобщението, ако вместо число изпратите текст, той ще каже: Въведохте невалидни данни, въведете число, и ще очаква от вас повторно въвеждане на данни. В случай, че сте изпратили число, ботът ще съобщи, че е приел вашата възраст, ще запише получените данни в базата, ще информира за всички получени от вас данни и ще преведе състоянието на чата в начално положение, т.е. в стартиране.
Вызвав метод state можете по всяко време да поискате текущото състояние на чата, а методът нулиране превежда чата в начално състояние.
Филтри на съобщенията
В нашия случай това е една от най-важните части при изграждането на бота. Именно с помощта на филтри за съобщения ботът ще разбира каква информация очаква от вас и как да я обработва.
В проекта на филтрите са описани във файла message_filters.R.
Код на филтрите за съобщения:
# ###########################################################
# 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"
}
)Във филтрите използваме предварително написаната функция get_state(), за да поискаме текущото състояние на чата. Тази функция изисква само 1 аргумент, id на чата.
След това филтърът wait_name обработва съобщения, когато чатът е в състояние wait_name, и съответно филтърът wait_age обработва съобщения, когато чатът е в състояние wait_age.
Обработчици
Файлът с обработчиците се нарича handlers.R, и има следния код:
# ###########################################################
# 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)Първо създаваме обработчици на команди, които ще ви позволят да стартирате методи за начало на диалога, неговото нулиране и запитване на текущото състояние.
След това създаваме 2 обработчика на съобщения с използване на създадените на предишния етап филтри и добавяме към тях филтър !MessageFilters$command, за да можем във всяко състояние на чата да използваме команди.
Код за стартиране на бота
Сега всичко е готово за стартиране, основният код за стартиране на бота се намира във файла 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()В резултат на това, получихме такъв бот:

Във всеки момент с помощта на командата /state можем да поискаме текущото състояние на чата, а с помощта на командата /reset да преведем чата в начално състояние и да започнем диалога отново.
Заключение
В тази статия разгледахме как да използваме бази данни вътре в бота и как да изграждаме последователни логически диалози, благодарение на фиксацията на състоянието на чата.
В този случай разгледахме най-примитивния пример, за да ви е по-лесно да разберете идеята за изграждане на такива ботове; на практика можете да изградите много по-сложни диалози.
В следващата статия от тази серия ще научим как да ограничим на потребителите на бота правата за използване на различни негови методи.
Източник: habr.com
