Глава 2 Добавляем боту поддержку команд и фильтры сообщений, класс Updater
Во второй главе нашего руководства мы продолжим развитие вашего Telegram-бота, сосредоточив внимание на добавлении команд и фильтров сообщений. Вы узнаете, как реализовать команды, которые пользователи могут отправлять боту, и как обрабатывать их с помощью пакета telegram.bot.
Мы рассмотрим, как настроить обработку команд для выполнения различных действий и внедрить фильтры для управления типами сообщений. Эти инструменты позволят вашему боту стать более интерактивным и удобным для пользователей.
Эта глава предоставит вам знания и навыки, необходимые для создания более функционального и адаптивного бота, который сможет эффективно взаимодействовать с аудиторией. Надеюсь, что изучение этих возможностей вдохновит вас на создание уникальных и полезных решений.
2.2 Класс Updater
Updater
- это класс, который упрощает вам разработку телеграм бота, и использует под капотом класс Dispetcher
. Назначение класса Updater
заключается в том, что бы получить обновления от бота (в предыдущей главе мы использовали для этой цели метод getUpdates()
), и передать их далее в Dispetcher
.
В свою очередь Dispetcher
содержит в себе созданные вами обработчики, т.е. объекты класса Handler
.
2.3 Handlers - обработчики
С помощью обработчиков вы добавляете в Dispetcher
реакции бота на различные события. На момент написания книги в telegram.bot
добавлены следующие типы обработчиков:
- MessageHandler - Обработчик сообщений
- CommandHandler - Обработчик команд
- CallbackQueryHandler - Обработчик данных отправляемых из Inline клавиатур
- ErrorHandler - Обработчик ошибок при запросе обновлений от бота
2.4 Добавляем первую команду боту, обработчик команд
Если вы никогда ранее не использовали ботов, и не в курсе, что такое команда, то команды боту необходимо отправлять с помощью прямого слеша /
в качестве префикса.
Начнём мы с простых команд, т.е. научим нашего бота здороваться по команде /hi
.
library(telegram.bot)
# создаём экземпляр класса Updater
updater <- Updater('ТОКЕН ВАШЕГО БОТА')
# Пишем метод для приветсвия
say_hello <- function(bot, update) {
# Имя пользователя с которым надо поздароваться
user_name <- update$effective_user()$first_name
# Отправка приветственного сообщения
bot$sendMessage(update$from_chat_id(),
text = paste0("Моё почтение, ", user_name, "!"),
parse_mode = "Markdown")
}
# создаём обработчик
hi_hendler <- CommandHandler('hi', say_hello)
# добаляем обработчик в диспетчер
updater <- updater + hi_hendler
# запускаем бота
updater$start_polling()
Запустите приведённый выше пример кода, предварительно заменив ‘ТОКЕН ВАШЕГО БОТА’ на реальный токен, который вы получили при создании бота через BotFather.
Метод start_polling()
класса Updater
, который используется в конце кода, запускает бесконечный цикл запроса и обработки обновлений от бота.
Мы написали боту метод say_hello()
. Методами бота являются функции с двумя обязательными аргументами:
- bot - объект бота, с помощью которого вы можете выполнять любые, доступные боту операции: отправлять сообщения, удалять сообщения, и так далее.
- update - полученное от пользоватя сообщение (обновление бота).
Внутри кода метода вы можете обращаться и к боту, и к обновлению. С методами бота мы познакомились в первой главе, теперь давайте я вкратце опишу методы, доступные в приходящих обновлениях:
-
from_chat_id()
- получить идентификатор чата, из которого боту было отправлено сообщение -
from_user_id()
- получить идентификатор пользователя, который отправил боту сообщение -
effective_chat()
- получить подробную информацию о чате, из которого бот получил сообщение -
effective_message()
- получить подробную информацию о сообщение, включая текс, вложения и т.д. -
effective_user()
- получить подробную информацию о пользователе, который отправил сообщение
Теперь откроем телеграм, и напишем нашему боту первую команду /hi
.
Теперь наш бот понимает команду /hi
, и умеет с нами здороваться.
Схематически процесс построения такого простейшего бота можно изобразить следующим образом.
- Создаём экземпляр класса
Updater
; - Создаём методы, т.е. функции которые будет выполнять наш бот. В примере кода это функция
say_hello()
. Функции, которые вами будут использоваться как методы бота должны иметь два обязательных аргумента - bot и update, и один необязательный - args. Аргумент bot, это и есть ваш бот, с его помощью вы можете отвечать на сообщения, отправлять сообщения, или использовать любые другие доступные боту методы. Аргумент update это то, что бот получил от пользователя, по сути, то что в первой главе мы получали методомgetUpdates()
. Аргумент args позволяет вам обрабатывать дополнительные данные отправленные пользователем вместе с командой, к этой теме мы ещё вернёмся немного позже; - Создаём обработчики, т.е. связываем какие-то действия пользователя с созданными на прошлом шаге методами. По сути обработчик это триггер, событие которое вызывает какую-то функцию бота. В нашем примере таким триггером является отправка команды
/hi
, и реализуется командойhi_hendler <- CommandHandler('hi', say_hello)
. Первый аргумент функцииCommandHandler()
позволяет вам задать команду, в нашем случаеhi
, на которую будет реагировать бот. Второй аргумент позволяет указать метод бота, мы будем вызывать методsay_hello
, который будет выполняться если пользователь вызвал указанную в первом аргументе команду; - Далее добавляем созданный обработчик в диспетчер нашего экземпляра класса
Updater
. Добавлять обработчики можно несколькими способами, в примере выше я использовал простейший, с помощью знака+
, т.е.updater <- updater + hi_hendler
. То же самое можно сделать с помощью методаadd_handler()
, который относится к классуDispatcher
, найти этот метод можно так:updater$dispatcher$add_handler()
; - Запускаем бота с помощью команды
start_polling()
.
2.5 Обработчик текстовых сообщений и фильтры
Как отправлять боту команды мы разобрались, но иногда нам требуется, что бы бот реагировал не только на команды, но и на какие-то обычные, текстовые сообщения. Для этого необходимо использовать обработчики сообщений - MessageHandler.
Обычный MessageHandler будет реагировать на абсолютно все входящие сообщения. Поэтому зачастую обработчики сообщений используются вместе с фильтрами. Давайте научим бота здороваться не только по команде /hi
, но и всегда, когда в сообщении отправленном боту встречается одно из следующих слов: привет, здравствуй, салют, хай, бонжур.
Пока мы не будем писать какие-то новые методы, т.к. у нас уже есть метод с помощью которого бот с нами здоровается. От нас требуется только создать нужный фильтр и обработчик сообщений.
library(telegram.bot)
# создаём экземпляр класса Updater
updater <- Updater('ТОКЕН ВАШЕГО БОТА')
# Пишем метод для приветсвия
## команда приветвия
say_hello <- function(bot, update) {
# Имя пользователя с которым надо поздароваться
user_name <- update$effective_user()$first_name
# Отправляем приветсвенное сообщение
bot$sendMessage(update$from_chat_id(),
text = paste0("Моё почтение, ", user_name, "!"),
parse_mode = "Markdown",
reply_to_message_id = update$message$message_id)
}
# создаём фильтры
MessageFilters$hi <- BaseFilter(function(message) {
# проверяем, встречается ли в тексте сообщения слова: привет, здравствуй, салют, хай, бонжур
grepl(x = message$text,
pattern = 'привет|здравствуй|салют|хай|бонжур',
ignore.case = TRUE)
}
)
# создаём обработчик
hi_hendler <- CommandHandler('hi', say_hello) # обработчик команды hi
hi_txt_hnd <- MessageHandler(say_hello, filters = MessageFilters$hi)
# добаляем обработчики в диспетчер
updater <- updater +
hi_hendler +
hi_txt_hnd
# запускаем бота
updater$start_polling()
Запустите приведённый выше пример кода, предварительно заменив ‘ТОКЕН ВАШЕГО БОТА’ на реальный токен, который вы получили при создании бота через BotFather.
Итак, в первую очередь мы научили бота не просто здороваться, а отвечать на приветствие. Сделали мы это с помощью аргумента reply_to_message_id, который доступен в методе sendMessage()
, в который необходимо передать id сообщения на которое требуется ответить. Получить id сообщения можно вот так: update$message$message_id
.
Но главное, что мы сделали - добавили боту фильтр с помощью функции BaseFilter()
:
# создаём фильтры
MessageFilters$hi <- BaseFilter(
# анонимная фильтрующая функция
function(message) {
# проверяем, встречается ли в тексте сообщения слова приветствия
grepl(x = message$text,
pattern = 'привет|здравствуй|салют|хай|бонжур',
ignore.case = TRUE)
}
)
Как вы могли заметить, фильтры необходимо добавлять в объект MessageFilters, в котором изначально уже есть небольшой набор готовых фильтров. В нашем примере в объект MessageFilters мы добавили элемент hi, это новый фильтр.
В функцию BaseFilter()
вам необходимо передать фильтрующую функцию. По сути, фильтр - это просто функция, которая получает экземпляр сообщения и возвращает TRUE или FALSE. В нашем примере, мы написали простейшую функцию, которая с помощью базовой функции grepl()
проверяет текст сообщения, и если он соответствует регулярному выражению привет|здравствуй|салют|хай|бонжур
возвращает TRUE.
Далее мы создаём обработчик сообщений hi_txt_hnd <- MessageHandler(say_hello, filters = MessageFilters$hi)
. Первый аргумент функции MessageHandler()
- метод, который будет вызывать обработчик, а второй аргумент - это фильтр по которому он будет вызываться. В нашем случае это созданный нами фильтр MessageFilters$hi
.
Ну и в итоге, мы добавляем в диспетчер созданный только, что обработчик hi_txt_hnd.
updater <- updater +
hi_hendler +
hi_txt_hnd
Как я уже писал выше, в пакете telegram.bot
и объекте MessageFilters уже есть набор встроенных фильтров, которые вы можете использовать:
- all - Все сообщения
- text - Текстовые сообщения
- command - Команды, т.е. сообщения которые начинаются на
/
- reply - Сообщения, которые являются ответом на другое сообщение
- audio - Сообщения в которых содержится аудио файл
- document - Сообщения с отправленным документом
- photo - Сообщения с отправленными изображениями
- sticker - Сообщения с отправленным стикером
- video - Сообщения с видео
- voice - Голосовые сообщения
- contact - Сообщения в которых содержится контант телеграм пользователя
- location - Сообщения с геолокацией
- venue - Пересылаемые сообщения
- game - Игры
Если вы хотите совместить некоторые фильтры в одном обработчике просто используйте знак |
- в качестве логического ИЛИ, и знак &
в качестве логического И. Например, если вы хотите что бы бот вызывал один и тот же метод когда он получает видео, изображение или документ используйте следующий пример создания обработчика сообщений:
handler <- MessageHandler(callback,
MessageFilters$video | MessageFilters$photo | MessageFilters$document
)
2.6 Добавление команд с параметрами
Мы уже знаем, что такое команды, как их создавать и как заставить бота выполнить нужную команду. Но в некоторых случаях помимо названия команды, нам необходимо передать некоторые данные для её выполнения.
Ниже пример бота, который по заданной дате и стране возвращает вам тип дня из производственного календаря.
Приведённый ниже бот использует API производственного календаря isdayoff.ru.
library(telegram.bot)
# создаём экземпляр класса Updater
updater <- Updater('ТОКЕН ВАШЕГО БОТА')
# Пишем метод для приветсвия
## команда приветвия
check_date <- function(bot, update, args) {
# входящие данные
day <- args[1] # дата
country <- args[2] # страна
# проверка введённых параметров
if ( !grepl('\\d{4}-\\d{2}-\\d{2}', day) ) {
# Send Custom Keyboard
bot$sendMessage(update$from_chat_id(),
text = paste0(day, " - некорреткная дата, введите дату в формате ГГГГ-ММ-ДД"),
parse_mode = "Markdown")
} else {
day <- as.Date(day)
# переводим в формат POSIXtl
y <- format(day, "%Y")
m <- format(day, "%m")
d <- format(day, "%d")
}
# страна для проверки
## проверяем задана ли страна
## если не задана устанавливаем ru
if ( ! country %in% c('ru', 'ua', 'by', 'kz', 'us') ) {
# Send Custom Keyboard
bot$sendMessage(update$from_chat_id(),
text = paste0(country, " - некорретктный код страны, возможнные значения: ru, by, kz, ua, us. Запрошены данные по России."),
parse_mode = "Markdown")
country <- 'ru'
}
# запрос данных из API
# компоновка HTTP запроса
url <- paste0("https://isdayoff.ru/api/getdata?",
"year=", y, "&",
"month=", m, "&",
"day=", d, "&",
"cc=", country, "&",
"pre=1&",
"covid=1")
# получаем ответ
res <- readLines(url)
# интрепретация ответа
out <- switch(res,
"0" = "Рабочий день",
"1" = "Нерабочий день",
"2" = "Сокращённый рабочий день",
"4" = "covid-19",
"100" = "Ошибка в дате",
"101" = "Данные не найдены",
"199" = "Ошибка сервиса")
# отправляем сообщение
bot$sendMessage(update$from_chat_id(),
text = paste0(day, " - ", out),
parse_mode = "Markdown")
}
# создаём обработчик
date_hendler <- CommandHandler('check_date', check_date, pass_args = TRUE)
# добаляем обработчик в диспетчер
updater <- updater + date_hendler
# запускаем бота
updater$start_polling()
Запустите приведённый выше пример кода, предварительно заменив ‘ТОКЕН ВАШЕГО БОТА’ на реальный токен, который вы получили при создании бота через BotFather.
Мы создали бота, который в арсенале имеет всего один метод check_date
, данный метод вызывается одноимённой командой.
Но, помимо имени команды, данный метод ждёт от вас введения двух параметров, код страны и дату. Далее бот проверяется, является ли заданный день в указанной стране выходным, сокращённым или рабочим согласно официального производственного календаря.
Что бы создаваемый нами метод принимал дополнительные параметры вместе с командой, используйте аргумент pass_args = TRUE
в функции CommandHandler()
, и при создании метода, помимо обязательных аргументов bot, update создайте опциональный - args. Созданный таким образом метод будет принимать параметры, которые вы передаёте боту после названия команды. Параметры необходимо между собой разделять пробелом, в метод они поступят в виде текстового вектора.
Давайте запустим, и протестируем нашего бота.
2.7 Запускаем бота в фоновом режиме
Последний шаг который нам осталось выполнить - запустить бота в фоновом режиме.
Для этого следуйте по описанному ниже алгоритму:
- Сохраните код бота в файл с расширением R. При работе в RStudio это делается через меню File, командой Save As….
- Добавьте путь к папке bin, которая в свою очередь находится в папке в которую вы установили язык R в переменную Path, инструкция тут.
- Создайте обычный текстовый файл, в котором пропишите 1 строку:
R CMD BATCH C:\Users\Alsey\Documents\my_bot.R
. Вместо *C:_bot.R* пропишите путь к своему скрипту бота. При этом важно, что бы в пути не встречалась кириллица и пробелы, т.к. это может вызвать проблемы при запуске бота. Сохраните его, и замените его расширение с txt на bat. - Откройте планировщик заданий Windows, есть множество способов это сделать, например откройте любую папку и в адресс введите
%windir%\system32\taskschd.msc /s
. Другие способы запуска можно найти тут. - В верхнем правом меню планировщика нажмите “Создать задачу…”.
- На вкладке “Общие” задайте произвольное имя вашей задаче, и переключатель перевидите в состояние “Выполнять для всех пользователей”.
- Перейдите на вкладку “Действия”, нажмите “Создать”. В поле “Программа или сценарий” нажмите “Обзор”, найдите созданный на втором шаге bat файл, и нажмите ОК.
- Жмём ОК, при необходимости вводим пароль от вашей учётной записи операционной системы.
- Находим в планировщике созданную задачу, выделяем и в нижнем правом углу жмём кнопку “Выполнить”.
Наш бот запущен в фоновом режиме, и будет работать до тех пор, пока вы не остановите задачу, или не выключите ваш ПК или сервер на котором его запустили.
2.8 Обработка голосовых сообщений. Переводим голосовое сообщение в текст
давайте разберём ещё один довольно полезный пример, напишем бота, который будет принимать голосовые сообщение, при чём эти голосовые сообщения можно пересылать из любого другого чата, а на выходе давать вам его текстовую расшифровку.
2.8.1 Функция для преобразования голоса в текст
Для начала нам необходимо разработать основу, т.е. функцию, которая на вход получит аудио файл, и преобразует его в текст. Для этого можно использовать Google Speech-to-Text API. Это условно бесплатный сервис, бесплатно вы можете в месяц конвертировать до 60 минут аудиоо в текст. Этот лимит в будущем возможно будет изменён.
Прежде всего, нам нужно настроить проект в Google Cloud:
- Зайдите на console.cloud.google.com и создайте новый проект.
- Включите API Speech-to-Text в разделе “APIs & Services”.
- Создайте учетные данные (Service Account Key) для доступа к API: 3.1. Перейдите в “APIs & Services” > “Credentials” 3.2. Нажмите “Create Credentials” > “Service Account Key” 3.3. Выберите роль “Project” > “Owner” 3.4. Скачайте JSON файл с ключом
Теперь установим пакеты, которые нам понадобятся:
install.packages(c("tuneR", "seewave", "googledrive", "googleAuthR", "googleLanguageR", "av"))
Теперь рассмотрим сам код функции, которая ляжет в основу нашего будущего бота:
library(tuneR)
library(seewave)
library(googledrive)
library(googleAuthR)
library(googleLanguageR)
library(av)
speech_to_text_from_audio <- function(audio_file_path) {
# Определяем расширение файла
file_ext <- tolower(tools::file_ext(audio_file_path))
# Создаем временный WAV файл
temp_wav_file <- tempfile(fileext = ".wav")
# Обработка в зависимости от типа файла
if (file_ext == "mp3") {
audio <- readMP3(audio_file_path)
} else if (file_ext == "wav") {
audio <- readWave(audio_file_path)
} else if (file_ext == "ogg") {
# Конвертируем OGG в WAV
av_audio_convert(audio_file_path, temp_wav_file)
audio <- readWave(temp_wav_file)
} else {
stop("Неподдерживаемый формат файла. Поддерживаются только MP3, WAV и OGG.")
}
# Если аудио стерео, конвертируем в моно
if (audio@stereo) {
audio <- mono(audio, "both")
}
# Изменяем частоту дискретизации на 16000 Гц, только если текущая частота отличается
if (audio@samp.rate != 16000) {
audio_resampled <- resamp(audio, g = 16000, output = "Wave")
} else {
audio_resampled <- audio
}
# Записываем обработанное аудио во временный WAV файл
writeWave(audio_resampled, temp_wav_file)
# Выполняем распознавание речи
result <- tryCatch({
gl_speech(temp_wav_file,
languageCode = "ru-RU",
sampleRateHertz = 16000)$transcript
}, error = function(e) {
return(paste("Ошибка при распознавании речи:", e$message))
})
# Удаляем временный WAV файл
file.remove(temp_wav_file)
# Возвращаем результат
return(result$transcript)
}
Голосовые сообщения в telegram хранятся в ogg формате, который данная функция успешно преобразовывает в текст. Для теста можете скачать voice.ogg файл и протестировать работу описанной выше функции.
Перед запуском не забудьте заменить “path/to/your/google_cloud_credentials.json” на путь к скачанному вами из Google Cloud ключу сервисного аккаунта.
# Пример использования:
# Не забудьте аутентифицироваться перед использованием функции
gl_auth("path/to/your/google_cloud_credentials.json")
# Теперь вы можете использовать функцию так:
ogg_file <- "path/to/your/voice.ogg"
transcript_ogg <- speech_to_text_from_audio(ogg_file)
На выходе получите следующий текст:
небольшая начитка для тестирования преобразования голоса в текст с помощью языка R
2.8.2 Код бота преобразуещего голосовое сообщение в текст
Теперь давайте напишем бота, в основе которого будет лежать данная функция, он будет получать любое голосовое сообщение, переводить его в текстовый формат и отправлять в виде текстового сообщения.
Примечание! Для того, что бы приведённый ниже код работал вам необходимо заранее сохранить токен бота в переменную среды, и заменить “my_bot” на указанное в названии переменной среды имя вашего бота. Так же необходимо заменить “path/to/your/google_cloud_credentials.json” на путь к скачанному вами из Google Cloud ключу сервисного аккаунта.
library(telegram.bot)
library(tuneR)
library(seewave)
library(googledrive)
library(googleAuthR)
library(googleLanguageR)
library(av)
# Функция для преобразования аудио в текст (используем ранее созданную функцию)
speech_to_text_from_audio <- function(audio_file_path) {
# Определяем расширение файла
file_ext <- tolower(tools::file_ext(audio_file_path))
# Создаем временный WAV файл
temp_wav_file <- tempfile(fileext = ".wav")
# Обработка в зависимости от типа файла
if (file_ext == "mp3") {
audio <- readMP3(audio_file_path)
} else if (file_ext == "wav") {
audio <- readWave(audio_file_path)
} else if (file_ext == "ogg") {
# Конвертируем OGG в WAV
av_audio_convert(audio_file_path, temp_wav_file)
audio <- readWave(temp_wav_file)
} else {
stop("Неподдерживаемый формат файла. Поддерживаются только MP3, WAV и OGG.")
}
# Если аудио стерео, конвертируем в моно
if (audio@stereo) {
audio <- mono(audio, "both")
}
# Изменяем частоту дискретизации на 16000 Гц, только если текущая частота отличается
if (audio@samp.rate != 16000) {
audio_resampled <- resamp(audio, g = 16000, output = "Wave")
} else {
audio_resampled <- audio
}
# Записываем обработанное аудио во временный WAV файл
writeWave(audio_resampled, temp_wav_file)
# Выполняем распознавание речи
result <- tryCatch({
gl_speech(temp_wav_file,
languageCode = "ru-RU",
sampleRateHertz = 16000)$transcript
}, error = function(e) {
return(paste("Ошибка при распознавании речи:", e$message))
})
# Удаляем временный WAV файл
file.remove(temp_wav_file)
# Возвращаем результат
return(result$transcript)
}
# Аутентификация в Google Cloud (делаем это перед запуском бота)
# Не забудьте аутентифицироваться в Google Cloud перед запуском бота
gl_auth("path/to/your/google_cloud_credentials.json")
# Функция для обработки голосовых сообщений
handle_voice <- function(bot, update) {
# Получаем информацию о голосовом сообщении
voice <- update$message$voice
# Получаем файл
file <- bot$getFile(voice$file_id)
# Создаем временный файл для сохранения голосового сообщения
temp_file <- tempfile(fileext = ".ogg")
# Получаем полный URL для скачивания файла
file_url <- paste0("https://api.telegram.org/file/bot", bot_token('my_bot'), "/", file$file_path)
# Скачиваем файл
download.file(file_url, temp_file, mode = "wb")
# Преобразуем голосовое сообщение в текст
text <- speech_to_text_from_audio(temp_file)
# Отправляем текст обратно пользователю
bot$sendMessage(chat_id = update$message$chat_id,
text = paste("Расшифровка вашего голосового сообщения:\n\n", text))
# Удаляем временный файл
file.remove(temp_file)
}
# Функция для обработки текстовых сообщений
handle_text <- function(bot, update) {
bot$sendMessage(chat_id = update$message$chat_id,
text = "Пожалуйста, отправьте голосовое сообщение для расшифровки.")
}
# Создание и настройка updater
updater <- Updater(bot_token('my_bot'))
# Добавляем обработчики с правильными фильтрами
updater <- updater + CommandHandler("start", handle_text)
updater <- updater + MessageHandler(handle_voice, MessageFilters$voice)
updater <- updater + MessageHandler(handle_text, MessageFilters$text)
# Запуск бота
updater$start_polling()
2.9 Бот для сбора статистики из Telegram чатов
Боты довольно функциональны, в том числе они могут быть модераторами чатов, или просто собирать статистику по активности в группах. В этом разделе я приведу пример бота, который собирает во внутреннюю базу данных статистику о сообщениях и новых учатсниках чатов, в которые он добавлен в роли администратора. Так же мы добавим боту команду, которая будет выводить статистику за любой указанный период.
Что бы бот мог собирать статистику из чата, или быть его модератором, у него обязательно должны быть в этом чате админские права.
2.9.1 Как добавить бота в группу
Для того, что бы использовать бота в публичных или закрытых группах, изначально проверьте соответвующую настройку в BotFather. По умолчанию эта настройка должна быть включена. Находится она тут: /mybots
-> @bot_username
-> Bot Settings -> Allow Groups?. Если настройка включена то вы увидите следующее сообщение:
Далее добааляете бота в нужные группы и используете его через команды. Если вам необходимо сделать так, что бы бот прослушивал не только команды, но и все сообщения в группе, то вам необходимо назначить его администратором, посе чего вы увидите что бот имеет доступ ко всем сообщениям.
2.9.2 Подготовка базы данных для хранения статистики
Ранее в этой книге мы рассматривали ботов, которые получают с данными полученными на лету, т.е. данные которые в моменте запрашиваются, используются и далее они нам не нужны. В данном случае задача в том, что бы собирать статистику из чатов, и потом при необходимости её запрашивать, поэтому для начала нам надо развернуть базу данных.
В нашем случае в базе будет всего 2 таблицы, в одну мы будем собирать данные о сообщениях, а в другую о добавленных пользователях. использовать мы будет встраивамаю в нашего бота SQLite базу.
Таблица для хранения данных о сообщениях:
-- message definition
CREATE TABLE `message` (
`chat_id` REAL,
`chat_title` TEXT,
`msg_id` INTEGER,
`timestamp` TEXT,
`date` TEXT,
`user_id` INTEGER,
`user_first_name` TEXT,
`user_last_name` TEXT,
`tg_username` TEXT,
`text` TEXT,
`is_link` INTEGER,
`links` INTEGER
);
Таблица для хранения данных о новых участниках:
-- new_users definition
CREATE TABLE `new_users` (
`chat_id` REAL,
`chat_title` TEXT,
`msg_id` INTEGER,
`timestamp` TEXT,
`date` TEXT,
`users_add_id` REAL,
`users_add_first_name` TEXT,
`users_add_last_name` TEXT,
`users_add_tg_username` TEXT,
`new_user_id` REAL,
`new_user_first_name` TEXT,
`new_user_last_name` TEXT,
`new_user_tg_username` TEXT
);
2.9.3 Методы бота
Для того, что бы код нашего бота был более читаем, код каждого из его методов я разделю на отдельные файлы, зачастую такой способ хранения функций используется при разработке пакетов. В нашем случае у бота будет всего 3 функции, давайте с помощью пакета usethis
создадим файлы для этих функций.
usethis::use_r('get_message') # функция для сбора данных о сообщениях
usethis::use_r('get_new_user') # функция для сбора данных о новых участниках
usethis::use_r('chat_stat') # функция запроса статистики по чатам
usethis::use_r('to_tg_table') # Вспомогательная функция приволящая data.frame в формат таблицы для отправки в telegram
Функцию to_tg_table()
мы с вами рассматривали в первой главе, остальные 3 функции разработаны специально для этого бота. Функция usethis::use_r()
создаёт в вашем проекте каталог R, и в нём файлы с заданными в единственном её аргументе названием.
теперь приведу пример кода для каждого метода:
get_message <- function(bot, update) {
msg <- update$effective_message()
# анализ ссылок в сообщении
if (map_lgl(msg$entities, ~ .x$type == "url") %>% sum(na.rm = T) %>% .[1] > 0) {
is_link <- TRUE
link <- map_chr(
msg$entities,
~ str_sub(msg$text, .x$offset, .x$offset + .x$length)
) %>%
str_remove_all('\\n') %>%
str_c(collapse = ', ')
str_sub(string = msg$text, msg$entities[[1]]$offset, msg$entities[[1]]$offset + msg$entities[[1]]$length)
} else {
is_link <- FALSE
link <- NA
}
msg_info <- tibble(
chat_id = msg$chat$id,
chat_title = msg$chat$title,
msg_id = msg$message_id,
timestamp = as.character(as.POSIXct(msg$date, origin = "1970-01-01")),
date = as.character(as.Date(as.POSIXlt(msg$date, origin = "1970-01-01"))),
user_id = msg$from$id,
user_first_name = ifelse(length(gsub("[^\x01-\x7F]", "", msg$from$first_name)) == 0, NA_character_, gsub("[^\x01-\x7F]", "", msg$from$first_name)),
user_last_name = ifelse(length(gsub("[^\x01-\x7F]", "", msg$from$last_name)) == 0, NA_character_, gsub("[^\x01-\x7F]", "", msg$from$last_name)),
tg_username = msg$from$username,
text = msg$text,
is_link = is_link,
links = link
)
con <- dbConnect(SQLite(), here(db_name))
dbWriteTable(conn = con, name = msg_tbl, value = msg_info, append = TRUE, overwrite = FALSE)
dbDisconnect(con)
}
Данная функция перехватает каждое текстовое сообщение в чате, далее проверяет его на наличие ссылок, и собирает информацию об этом сообщении:
- Идентификатор чата в котором было перехвачено это сообщение
- Название чата
- Идентификатор сообщения
- Дата и время когда было отправлено сообщение
- Дата когда было отправлено сообщение
- ID пользователя в telegram, который отправил сообщение
- Имя пользователя (если указано в telegram)
- Фамилия пользователя (если указано в telegram)
- Username в telegram
- текст сообщения
- есть ли в сообщении хотя бы одна ссылка
- Представленные в сообщение ссылки через запятую
Далее функция записывает собранную информацию в таблицу message нашей базы данных.
get_new_user <- function(bot, update) {
msg <- update$effective_message()
msg_info <- tibble(
chat_id = msg$chat$id,
chat_title = msg$chat$title,
msg_id = msg$message_id,
timestamp = as.character(as.POSIXct(msg$date, origin = "1970-01-01")),
date = as.character(as.Date(as.POSIXlt(msg$date, origin = "1970-01-01"))),
users_add_id = msg$from$id,
users_add_first_name = ifelse(length(gsub("[^\x01-\x7F]", "", msg$from$first_name)) == 0, NA_character_, gsub("[^\x01-\x7F]", "", msg$from$first_name)),
users_add_last_name = ifelse(length(gsub("[^\x01-\x7F]", "", msg$from$last_name)) == 0, NA_character_, gsub("[^\x01-\x7F]", "", msg$from$last_name)),
users_add_tg_username = msg$from$username,
new_user_id = msg$new_chat_member$id,
new_user_first_name = ifelse(length(gsub("[^\x01-\x7F]", "", msg$new_chat_member$first_name)) == 0, NA_character_, gsub("[^\x01-\x7F]", "", msg$new_chat_member$first_name)),
new_user_last_name = ifelse(length(gsub("[^\x01-\x7F]", "", msg$new_chat_member$last_name)) == 0, NA_character_, gsub("[^\x01-\x7F]", "", msg$new_chat_member$last_name)),
new_user_tg_username = msg$new_chat_member$username
)
con <- dbConnect(SQLite(), here(db_name))
dbWriteTable(conn = con, name = new_user_tbl, value = msg_info, append = TRUE, overwrite = FALSE)
dbDisconnect(con)
}
Данная функция схожа с предыдущей, но она собирает информацию о добавленном участнике чата.
- Идентификатор чата в котором было перехвачено это сообщение
- Название чата
- Идентификатор сообщения
- дата и время когда был добавлен пользователь
- Дата когда был добавлен пользователь
- Идентификатор пользователя, который добавил новго участника в чат
- Имя пользователя, который добавил новго участника в чат (если указано в telegram)
- Фамилия пользователя, который добавил новго участника в чат (если указано в telegram)
- Username в telegram, который добавил новго участника в чат
- Идентификатор добавленого участника
- Имя пользователя, добавленого участника (если указано в telegram)
- Фамилия пользователя, добавленого участника (если указано в telegram)
- Username в telegram, добавленого участника
Собранную информацию данный метод дописывает в таблицу new_users.
chat_stat <- function(bot, update, args) {
function_list <- ls("package:timeperiodsR") %>% .[grepl(pattern = '^previous|^this', x = .)]
func_names <- str_c(ls("package:timeperiodsR") %>% .[grepl(pattern = "^previous|^this", x = .)], collapse = ", ")
if (is.null(args)) {
bot$sendMessage(
update$from_chat_id(),
text = str_glue('Вы не указали период за который необходимо получить статистику, доступные периоды: {func_names}. \n По умолчанию устанавливаю период this_month.'),
parse_mode = 'html'
)
args <- 'this_month'
}
if (!args[1] %in% function_list) {
bot$sendMessage(
update$from_chat_id(),
text = str_glue('Вы указали некорректный период ({args}), доступные периоды: {func_names}. \n По умолчанию устанавливаю период this_month.'),
parse_mode = 'html'
)
args <- 'this_month'
}
bot$sendChatAction(
update$from_chat_id(),
action = "typing"
)
period <- do.call(args[1], list(x = Sys.Date()))
con <- dbConnect(SQLite(), here(db_name))
messages <- dbGetQuery(
conn = con,
statement = str_glue(
'SELECT *
FROM message
WHERE date(date) BETWEEN "{period$start}" AND "{period$end}" '
))
new_users <- dbGetQuery(
conn = con,
statement = str_glue(
'SELECT *
FROM new_users
WHERE date(date) BETWEEN "{period$start}" AND "{period$end}" '
))
dbDisconnect(con)
chats <- unique(messages$chat_title)
for (chat in chats) {
chat_msg_data <- filter(messages, chat_title == chat)
chat_new_users <- filter(new_users, chat_title == chat)
active_users <- n_distinct(chat_msg_data$user_id)
total_msg <- n_distinct(chat_msg_data$msg_id)
events_links <- filter(chat_msg_data, str_detect(links, 'calendar.google.com')) %>% nrow()
total_links <- sum(chat_msg_data$is_link)
new_users_n <- nrow(chat_new_users)
top_active_users <- chat_msg_data %>%
count(tg_username, user_first_name, user_last_name, sort = T) %>%
mutate(user_last_name = replace_na(user_last_name, '')) %>%
mutate(tg_username = if_else(is.na(tg_username), str_glue('{user_first_name} {user_last_name}'), tg_username)) %>%
select(tg_username, n) %>%
slice_head(n = 5) %>%
mutate(tg_username = str_to_lower(tg_username)) %>%
select('contacts_telegram', 'n') %>%
rename( msg = n )
ggplot(top_active_users, aes(y = forcats::fct_reorder(contacts_telegram, msg, median), x = msg)) +
geom_col(aes(fill = msg)) +
theme(axis.text.y = element_text(size = 8),
plot.subtitle = element_text(size = 10)) +
scale_fill_gradient(high=hcl(15,100,75), low=hcl(195,100,75)) +
xlab("Ник") +
ylab('К-во сообщений') +
guides(fill=FALSE) +
ggtitle(label = chat, subtitle = 'Top 5 участников по активности')
ggsave('top5.png', device = 'png', units = 'cm', width = 13, height = 8)
top_active_users <- to_tg_table(top_active_users)
tg_msg <- str_glue(
'Чат *{chat}*',
'Период: {format(period$start, "%d.%m.%Y")} - {format(period$end, "%d.%m.%Y")}',
'',
'Общая статистика:',
'Активных пользователей: {active_users}',
'Всего сообщений: {total_msg}',
'Сообщений с ссылками: {total_links}',
'Новых пользователей: {new_users_n}',
'',
'Самые активные пользователи:',
top_active_users,
.sep = '\n'
)
bot$sendPhoto(
update$from_chat_id(),
photo = 'top5.png',
caption = tg_msg,
parse_mode = 'Markdown'
)
Sys.sleep(1)
}
}
Данная функция запрашивает из базы данных собранную статистику за указанный период, формирует график и таблицу и отправляет вам в telegram.
Этот метод позволяет передать аргумент, в качестве аргумента вы моете указать произвольный период, за который зотите получить статистику. В качестве преиода необходимо указать одну из функций пакета timeperiodsR
с префиксом previous
млм this
, например this_month
, previous_quarter
и т.д.
Далее запрашивает за указанный период статистику из таблиц message и new_users, на основе статистики строит таблицу и график по 5 наиболее активным пользователям за указанный период. Формирует и отправляет сообщение со статистикой, выглядит в итоге оно примерно так:
# функция для перевода data.frame в telegram таблицу
to_tg_table <- function( table, align = NULL, indents = 3, parse_mode = 'Markdown' ) {
# если выравнивание не задано то выравниваем по левому краю
if ( is.null(align) ) {
col_num <- length(table)
align <- str_c( rep('l', col_num), collapse = '' )
}
# проверяем правильно ли заданно выравнивание
if ( length(table) != nchar(align) ) {
align <- NULL
}
# новое выравнивание столбцов
side <- sapply(1:nchar(align),
function(x) {
letter <- substr(align, x, x)
switch (letter,
'l' = 'right',
'r' = 'left',
'c' = 'both',
'left'
)
})
# сохраняем имена
t_names <- names(table)
# вычисляем ширину столбцов
names_length <- sapply(t_names, nchar)
value_length <- sapply(table, function(x) max(nchar(as.character(x))))
max_length <- ifelse(value_length > names_length, value_length, names_length)
# подгоняем размер имён столбцов под их ширину + указанное в indents к-во пробелов
t_names <- mapply(str_pad,
string = t_names,
width = max_length + indents,
side = side)
# объединяем названия столбцов
str_names <- str_c(t_names, collapse = '')
# аргументы для фукнции str_pad
rules <- list(string = table, width = max_length + indents, side = side)
# поочереди переводим каждый столбец к нужному виду
t_str <- pmap_df( rules, str_pad )%>%
unite("data", everything(), remove = TRUE, sep = '') %>%
unlist(data) %>%
str_c(collapse = '\n')
# если таблица занимает более 4096 символов обрезаем её
if ( nchar(t_str) >= 4021 ) {
warning('Таблица составляет более 4096 символов!')
t_str <- substr(t_str, 1, 4021)
}
# символы выделения блока кода согласно выбранной разметке
code_block <- switch(parse_mode,
'Markdown' = c('```', '```'),
'HTML' = c('<code>', '</code>'))
# переводим в code
res <- str_c(code_block[1], str_names, t_str, code_block[2], sep = '\n')
return(res)
}
Эту функцию мы уже разбирали в первой главе книги.
2.9.4 Код бота сбора статистики
С методами бота мы разобрались, ниже привожу пример кода самого бота. Не забудьте подставить имя своего бота в функцию bot_token()
, или замените её на токен вашего бота.
Sys.setlocale(locale = 'russian')
library(telegram.bot)
library(RSQLite)
library(purrr)
library(stringr)
library(dplyr)
library(here)
library(tidyr)
library(timeperiodsR)
library(ggplot2)
# Переменные
db_name <- 'chatstat.db'
msg_tbl <- 'message'
new_user_tbl <- 'new_users'
bot_start_time <- Sys.time()
# Чтение методов бота из каталога R
funcs <- dir(here('R'))
walk(here("R", funcs), source)
# Инициализация бота
updater <- Updater(bot_token("Chat stat bot"))
updater$bot$clean_updates()
# фильтры
# фильтр для обработки сообщений msg
MessageFilters$save_msg <- BaseFilter(function(message) {
if ( !is.null(message$text) ) {
TRUE
} else {
FALSE
}
}
)
# фильтр для обработки добавленных в чат участников
MessageFilters$new_users <- BaseFilter(function(message) {
if ( !is.null(message$new_chat_member) ) {
TRUE
} else {
FALSE
}
}
)
# обработчики
h_save_msg <- MessageHandler(get_message, MessageFilters$save_msg & ! MessageFilters$command)
h_new_user <- MessageHandler(get_new_user, MessageFilters$new_users & ! MessageFilters$command)
h_stat <- CommandHandler('chat_stat', chat_stat, pass_args = T)
# диспетчер
updater <-
updater +
h_save_msg +
h_new_user +
h_stat
# запуск бота
updater$start_polling()
Далее добавляете созданного вами бота в ваши чаты, даёте ему права администратора, и он в фоновом режиме собирает статистику в свою внутреннюю базу. При необходимости можно использовать вместо SQLite какую то клиент серверную или облачную базу, в таком случае на основе собранной статистику без проблем вы сможете строить дашборды в различных BI системах.
Боты могут не только собирать статистику по активности в чатах, но так же модерировать их, считать карму её участников, отвечать на какие либо вопросы участников чата.
2.10 Как добавить описание команд в интерфейс бота
Теперь вы умеете создавать полноценных ботов, которых помимо вас могут использовать другие пользователи. Но, для того, что бы облегчить поиск нужных команд вы можете добавить их в интефейс бота.
Делается это через BotFather -> @bot_username
-> Edit Bot -> Edit Commands. Далее просто передаёте название команды и через тире их описание:
command1 - Description
command2 - Another description
2.11 Заключение
Отличная работа! Вы освоили настройку команд и фильтров для вашего бота. Это основа для создания более сложных взаимодействий и функций. В следующей главе мы будем работать с клавиатурами — как reply, так и inline, чтобы улучшить пользовательский интерфейс и взаимодействие с вашим ботом. Будьте готовы к тому, чтобы сделать ваш бот более интерактивным и удобным для пользователей.
2.12 Тесты и задания
2.12.1 Тесты
Для закрепления материла рекомендую вам пройти тест доступный по ссылке.