Jeśli już zapoznaliście się z poprzednimi z tej serii, to już potraficie pisać pełnoprawnych botów telegramowych z klawiaturą.
W tym artykule nauczymy się, jak napisać bota, który będzie wspierał sekwencyjny dialog. Tzn. bot będzie zadawał wam pytania i czekał na wprowadzenie jakiejkolwiek informacji. W zależności od wprowadzonych przez was danych, bot będzie wykonywał różne działania.
Nauczymy się również wykorzystywać do działania bota bazę danych, w naszym przykładzie będzie to SQLite, ale możecie użyć dowolnego innego systemu zarządzania bazami danych. Bardziej szczegółowo o interakcji z bazami danych pisaliśmy w .

Wszystkie artykuły z serii „Tworzymy bota Telegram w języku R”
Spis treści
Jeśli interesujesz się analizą danych, to mogą Cię zainteresować moje i kanały. Większość zawartości poświęcona jest językowi R.
Wprowadzenie
Aby bot mógł prosić was o dane i czekać na wprowadzenie jakichkolwiek informacji, będziecie musieli zarejestrować bieżący stan dialogu. Najlepszym sposobem, by to osiągnąć, jest użycie jakiejś wbudowanej bazy danych, na przykład SQLite.
Tzn. logika będzie wyglądać następująco. Wywołujemy metodę bota, a bot kolejno prosi nas o jakąś informację, przy tym na każdym kroku czeka na wprowadzenie tych informacji i może dokonywać ich sprawdzenia.
Napiszemy maksymalnie prostego bota, na początku zapyta on o twoje imię, później o wiek, a zdobyte dane zapisze w bazie danych. Przy zapytaniu o wiek sprawdzi, czy wprowadzone dane są liczbą, a nie tekstem.
Taki prosty dialog będzie miał tylko trzy stany:
- start — normalny stan bota, w którym nie czeka na od was żadnych informacji
- wait_name — stan, w którym bot oczekuje na wprowadzenie imienia
- wait_age — stan, w którym bot oczekuje na wprowadzenie waszego wieku, liczby pełnych lat.
Proces budowy bota
W trakcie artykułu krok po kroku stworzymy bota, cały proces można schematycznie przedstawić w następujący sposób:

- Tworzymy konfigurację bota, w której będziemy przechowywać pewne ustawienia. W naszym przypadku token bota oraz ścieżkę do pliku bazy danych.
- Tworzymy zmienną środowiskową, w której będzie przechowywana ścieżka do projektu z botem.
- Tworzymy bazę danych oraz szereg funkcji, dzięki którym bot będzie mógł z nią współpracować.
- Pisanie metod bota, czyli funkcji, które będzie wykonywał.
- Dodajemy filtry wiadomości, dzięki którym bot będzie mógł odwoływać się do odpowiednich metod, w zależności od aktualnego stanu czatu.
- Dodajemy obsługiwacze, które połączą polecenia i wiadomości z odpowiednimi metodami bota.
- Uruchamiamy bota.
Struktura projektu bota
Dla wygody podzielimy kod naszego bota oraz inne związane z nim pliki na następującą strukturę.
- bot.R — główny kod naszego bota
- db_bot_function.R — blok kodu z funkcjami do pracy z bazą danych
- bot_methods.R — kod metod bota
- message_filters.R — filtry wiadomości
- handlers.R — obsługiwacze
- config.cfg — konfiguracja bota
- create_db_data.sql — skrypt SQL do tworzenia tabeli z danymi czatu w bazie danych
- create_db_state.sql — skrypt SQL do tworzenia tabeli aktualnego stanu czatu w bazie danych
- bot.db — baza danych bota
Cały projekt bota można zobaczyć, lub z mojego .
Konfiguracja bota
Jako konfigurację użyjemy standardowego , w następującym formacie:
[bot_settings]
bot_token=TWÓJ_TOKEN_BOTA
[db_settings]
db_path=C:/ŚCIEŻKA/DO/FOLDERU/PROJEKTU/bot.dbW konfiguracji zapisujemy token bota oraz ścieżkę do bazy danych, tzn. do pliku bot.db, którego stworzymy w następnej chwili.
Dla bardziej złożonych botów można tworzyć również bardziej skomplikowane konfiguracje, ponadto nie ma obowiązku używania formatu ini, można używać dowolnego formatu, w tym JSON.
Tworzymy zmienną środowiskową
Na każdym komputerze folder z projektem bota może znajdować się w różnych lokalizacjach i na różnych dyskach, dlatego w kodzie ścieżka do folderu projektu zostanie określona przez zmienną środowiskową TG_BOT_PATH.
Zmienną środowiskową można utworzyć na kilka sposobów, najprostszym jest zapisanie jej w pliku .Renviron.
Można utworzyć lub edytować ten plik za pomocą polecenia file.edit(path.expand(file.path("~", ".Renviron"))). Wykonaj je i dodaj w pliku jeden wiersz:
TG_BOT_PATH=C:/ŚCIEŻKA/DO/TWOJEGO/PROJEKTUNastępnie zapisz plik .Renviron i zrestartuj RStudio.
Tworzymy bazę danych
Następny krok to stworzenie bazy danych. Potrzebujemy 2 tabel:
- chat_data — dane, które bot uzyskał od użytkownika
- chat_state — aktualny stan wszystkich czatów
Te tabele można stworzyć za pomocą następującego zapytania 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
);
Jeśli pobrałeś projekt bota z , aby stworzyć bazę danych, możesz skorzystać z poniższego kodu w języku 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'))
Pisanie funkcji do pracy z bazą danych
Mamy już gotowy plik konfiguracyjny i utworzoną bazę danych. Teraz musimy napisać funkcje do odczytu i zapisu danych do tej bazy.
Jeśli pobrałeś projekt z , to funkcje możesz znaleźć w pliku db_bot_function.R.
Kod funkcji do pracy z bazą danych
# ###########################################################
# 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]])
}Utworzyliśmy 4 proste funkcje:
get_state()— uzyskać aktualny stan czatu z bazy danychset_state()— zapisać aktualny stan czatu w bazie danychget_chat_data()— uzyskać dane przesłane przez użytkownikaset_chat_data()— zapisać dane otrzymane od użytkownika
Wszystkie funkcje są dość proste, albo odczytują dane z bazy za pomocą komendy dbGetQuery(), albo wykonują UPSERT operację (zmiana istniejących danych lub zapis nowych danych w bazie), za pomocą funkcji dbExecute().
Składnia operacji UPSERT wygląda następująco:
INSERT INTO chat_data (chat_id, ${field})
VALUES(${chat_id}, '${value}')
ON CONFLICT(chat_id)
DO UPDATE SET ${field}='${value}';Tzn. w naszych tabelach pole chat_id ma ograniczenie unikalności i jest kluczem podstawowym tabel. Początkowo próbujemy dodać informacje do tabeli, a w przypadku błędu, gdy dane dla aktualnego czatu już istnieją, po prostu aktualizujemy informacje dla danego czatu.
Następnie te funkcje będziemy używać w metodach i filtrach bota.
Metody bota
Następnym krokiem w budowie naszego bota będzie stworzenie metod. Jeśli pobrałeś projekt z , to wszystkie metody znajdują się w pliku bot_methods.R.
Kod metod bota
# ###########################################################
# 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')
}
}Utworzyliśmy 5 metod:
- start — Rozpoczęcie dialogu
- state — Uzyskanie aktualnego stanu czatu
- reset — Resetowanie aktualnego stanu czatu
- enter_name — Bot pyta o twoje imię
- enter_age — Bot pyta o twój wiek
Metoda start pyta o twoje imię i zmienia stan czatu na wait_name, tzn. w tryb oczekiwania na wprowadzenie twojego imienia.
Następnie wysyłasz imię, a ono jest przetwarzane przez metodę enter_name, bot cię wita, zapisuje otrzymane imię w bazie i zmienia czat na stan wait_age.
Na tym etapie bot czeka na wprowadzenie twojego wieku. Wysyłasz swój wiek, bot sprawdza wiadomość, a jeśli zamiast liczby wysłałeś jakiś tekst, powie: Wprowadziłeś niepoprawne dane, wprowadź liczbę., i będzie oczekiwał od ciebie ponownego wprowadzenia danych. Jeśli wysłałeś liczbę, bot poinformuje cię, że przyjął twój wiek, zapisze otrzymane dane w bazie, przekaże wszystkie uzyskane od ciebie informacje i przywróci stan czatu do pierwotnego, tj. do start.
Wywołując metodę stan możesz w każdej chwili zapytać o bieżący stan czatu, a metodą reset przywrócić czat do pierwotnego stanu.
Filtry wiadomości
W naszym przypadku to jedna z najważniejszych części budowy bota. To właśnie dzięki filtrom wiadomości bot będzie rozumiał, jaką informację od ciebie oczekuje, i jak ją przetworzyć.
W projekcie na filtrowanie zostało zapisane w pliku message_filters.R.
Kod filtrów wiadomości:
# ###########################################################
# 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"
}
)W filtrach wykorzystujemy wcześniej napisaną funkcję get_state(), aby zapytać o bieżący stan czatu. Ta funkcja wymaga tylko 1 argumentu, id czatu.
Następnie filtr wait_name przetwarza wiadomości, gdy czat znajduje się w stanie wait_name, a odpowiednio filtr wait_age przetwarza wiadomości, gdy czat znajduje się w stanie wait_age.
Obsługiwacze
Plik z obsługującymi go funkcjami nosi nazwę handlers.R, i zawiera następujący kod:
# ###########################################################
# 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)Najpierw tworzymy obsługujące komendy, które pozwolą ci uruchamiać metody do rozpoczęcia dialogu, jego resetowania i zapytania o bieżący stan.
Następnie tworzymy 2 obsługujące wiadomości przy użyciu wcześniej stworzonych filtrów i dodajemy do nich filtr !MessageFilters$command, abyśmy w dowolnym stanie czatu mogli używać komend.
Kod uruchamiający bota
Teraz wszystko jest gotowe do uruchomienia, główny kod uruchamiający bota znajduje się w pliku bot.R.
library(telegram.bot)
library(tidyverse)
library(RSQLite)
library(DBI)
library(configr)
# przechodzimy do folderu projektu
setwd(Sys.getenv('TG_BOT_PATH'))
# czytamy konfigurację
cfg <- read.config('config.cfg')
# tworzymy instancję bota
updater <- Updater(cfg$bot_settings$bot_token)
# Ładowanie komponentów bota
source('db_bot_function.R') # funkcje do pracy z bazą danych
source('bot_methods.R') # metody bota
source('message_filters.R') # filtry wiadomości
source('handlers.R') # obsługujące wiadomości
# Dodajemy obsługujące do menedżera
updater <- updater +
start_h +
wait_age_h +
wait_name_h +
state_h +
reset_h
# Uruchamiamy bota
updater$start_polling()W rezultacie otrzymaliśmy takiego bota:

W każdej chwili za pomocą komendy /state możemy zapytać o bieżący stan czatu, a za pomocą komendy /reset przywrócić czat do pierwotnego stanu i rozpocząć dialog od nowa.
Podsumowanie
W tym artykule zrozumieliśmy, jak używać baz danych w botach, oraz jak budować sekwencyjne logiczne dialogi poprzez rejestrowanie stanu czatu.
W tym przypadku przyjrzeliśmy się najbardziej podstawowemu przykładzie, aby łatwiej zrozumieć ideę budowania takich botów; w praktyce można tworzyć znacznie bardziej złożone dialogi.
W następnym artykule z tej serii nauczymy się ograniczać użytkownikom bota dostęp do różnych jego metod.
Źródło: habr.com
