Пишем Telegram бот на езика R (част 4): Изграждане на последователен, логичен диалог с бота

Ако вече сте запознали с предишните три статии от тази серия, вече можете да пишете полноценни telegram ботове с клавиатура.

В тази статия ще научим как да напишем бот, който ще поддържа последователен диалог. Т.е. ботът ще ви задава въпроси и ще изчаква от вас да въведете информация. В зависимост от въведените от вас данни, ботът ще изпълнява определени действия.

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

Пишем Telegram бот на езика R (част 4): Изграждане на последователен, логичен диалог с бота

Всички статии от серията "Пишем телеграм бот на езика R"

  1. Създаваме бот и изпращаме съобщения с него в телеграм
  2. Добавяме на бота поддръжка за команди и филтри на съобщенията
  3. Как да добавим поддръжка на клавиатура към бота
  4. Изграждане на последователен, логичен диалог с бота

Съдържание

Ако се интересувате от анализ на данни, вероятно ще ви бъдат интересни моите telegram и youtube канали. Голямата част от съдържанието, което се отнася до езика R.

  1. Въведение
  2. Процесът на изграждане на бота
  3. Структура на проекта на бота
  4. Конфигурацията на бота
  5. Създаване на променлива за средата
  6. Създаване на база данни
  7. Писане на функции за работа с базата данни
  8. Методи на бота
  9. Филтри на съобщенията
  10. Обработчици
  11. Код за стартиране на бота
  12. Заключение

Въведение

За да може ботът да изисква данни от вас и да изчаква въвеждането на информация, е необходимо да запомните текущото състояние на диалога. Най-добрият начин да го направите е да използвате вградена база данни, например SQLite.

Т.е. логиката ще следва следното. Извикваме метод на бота, и той последователно изисква от нас информация, като на всяка стъпка изчаква въвеждането на тази информация и може да я проверява.

Ще напишем максимално прост бот, който първо ще пита за вашето име, след това за възраст, като получените данни ще се запазват в базата данни. При запитване за възрастта той ще проверява дали въведените данни са число, а не текст.

Такъв прост диалог ще има само три състояния:

  1. start — обичното състояние на бота, в което той не очаква от вас никаква информация
  2. wait_name — състояние, при което ботът очаква въвеждане на име
  3. wait_age — състояние, при което ботът очаква въвеждане на вашата възраст, брой завършени години.

Процесът на изграждане на бота

В хода на статията ще изградим бота стъпка по стъпка, целият процес може схематично да бъде представен по следния начин:
Пишем Telegram бот на езика R (част 4): Изграждане на последователен, логичен диалог с бота

  1. Създаваме конфигурация на бота, в която ще съхранляваме някои настройки. В нашия случай токена на бота и пътя до файла на базата данни.
  2. Създаваме променлива за средата, в която ще се съхранява пътя до проекта с бота.
  3. Създаваме самата база данни и редица функции, за да може ботът да взаимодейства с нея.
  4. Пишем методите на бота, т.е. функциите, които той ще изпълнява.
  5. Добавяме филтри за съобщения. С помощта на които ботът ще се обръща към нужните методи в зависимост от текущото състояние на чата.
  6. Добавяме обработчици, които свързват командите и съобщенията с нужните методи на бота.
  7. Стартираме бота.

Структура на проекта на бота

За удобство ще разделим кода на нашия бот и другите свързани с него файлове на следната структура.

  • 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 — базата данни на бота

Целият проект на бота може да бъде видян, или да се изтегли от моят репозитория на GitHub.

Конфигурацията на бота

Като конфиг ще използваме обикновен ini файл, следния вид:

[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
);

Ако сте изтеглили проекта на бота от GitHub, за да създадете база, можете да използвате следния код на езика 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'))

Писане на функции за работа с базата данни

Вече имаме готов файл за конфигуриране и създадена база данни. Сега е нужно да напишем функции за четене и запис на данни в тази база.

Ако сте изтеглили проекта от GitHub, можете да намерите функциите във файла 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 има ограничение за уникалност и е първичен ключ на таблиците. Първоначално пробваме да добавим информация в таблицата, и получаваме грешка, ако данните за текущия чат вече присъстват, в такъв случай просто актуализираме информацията за този чат.

Следващите функции ще използваме в методите и филтрите на бота.

Методи на бота

Следващата стъпка при изграждането на нашия бот ще бъде създаването на методи. Ако сте изтеглили проекта от GitHub, всички методи се намират в файла 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 можете по всяко време да поискате текущото състояние на чата, а методът нулиране превежда чата в начално състояние.

Филтри на съобщенията

В нашия случай това е една от най-важните части при изграждането на бота. Именно с помощта на филтри за съобщения ботът ще разбира каква информация очаква от вас и как да я обработва.

В проекта на GitHub филтрите са описани във файла 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()

В резултат на това, получихме такъв бот:
Пишем Telegram бот на езика R (част 4): Изграждане на последователен, логичен диалог с бота

Във всеки момент с помощта на командата /state можем да поискаме текущото състояние на чата, а с помощта на командата /reset да преведем чата в начално състояние и да започнем диалога отново.

Заключение

В тази статия разгледахме как да използваме бази данни вътре в бота и как да изграждаме последователни логически диалози, благодарение на фиксацията на състоянието на чата.

В този случай разгледахме най-примитивния пример, за да ви е по-лесно да разберете идеята за изграждане на такива ботове; на практика можете да изградите много по-сложни диалози.

В следващата статия от тази серия ще научим как да ограничим на потребителите на бота правата за използване на различни негови методи.

Източник: habr.com

Купете надежден хостинг за сайтове със защита от DDoS, VPS и VDS сървъри 🔥 Купете надежден хостинг за сайтове със защита от DDoS, VPS и VDS сървъри | ProHoster