Пишемо telegram бота мовою R (частина 4): Побудова послідовного, логічного діалогу з ботом

Якщо ви вже ознайомились із попередніми трьома статтями з цієї серії, ви вже вмієте писати повноцінних telegram ботів з клавіатурою.

У цій статті ми з вами навчимося писати бота, який підтримуватиме послідовний діалог. Тобто. бот буде ставити вам питання, і чекати від вас введення будь-якої інформації. Залежно від введених вами даних бот виконуватиме деякі дії.

Також у цій статті ми навчимося використовувати під капотом бот бази даних, у нашому прикладі це буде SQLite, але ви можете використовувати будь-яку іншу СУБД. Більш детально про взаємодію з базами даних мовою R я писав у цієї статті.

Пишемо telegram бота мовою R (частина 4): Побудова послідовного, логічного діалогу з ботом

Всі статті із серії "Пишемо telegram бота мовою R"

  1. Створюємо бота і відправляємо за його допомогою повідомлення в telegram
  2. Додаємо боту підтримку команд та фільтри повідомлень
  3. Як додати боту підтримку клавіатури
  4. Побудова послідовного, логічного діалогу з ботом

Зміст

Якщо ви цікавитеся аналізом даних можливо вам будуть цікаві мої телеграма и 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 - Бот запитує ваш вік

метод start запитує ваше ім'я, і ​​переводить стан чату в wait_name, тобто. у режим очікування введення вашого імені.

Далі ви відправляєте ім'я і воно обробляється методом enter_name, робота з вами вітається, записує отримане ім'я в базу, і переводить чат в стан wait_age.

На цьому етапі бот чекає від вас уведення вашого віку. Ви відправляєте ваш вік, бот перевіряє повідомлення, якщо ви замість числа відправили якийсь текст він скаже: Ты ввёл некорректные данные, введи число, і буде чекати від вас повторного введення даних. Якщо ви відправили число, робот повідомить про те, що він прийняв ваш вік, запише отримані дані в базу, повідомить усі отримані від вас дані і переведе стан чату у вихідне положення, тобто. в start.

Викликавши метод state Ви можете запросити поточний стан чату, а методом reset перевести чат у вихідний стан.

Фільтри повідомлень

У нашому випадку це одна з найважливіших елементів у побудові робота. Саме за допомогою фільтрів повідомлень бот розумітиме яку інформацію він від вас чекає, і як її треба обробляти.

У проекті на 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

Додати коментар або відгук