Глава 2 Добавляем боту поддержку команд и фильтры сообщений, класс Updater

Во второй главе нашего руководства мы продолжим развитие вашего Telegram-бота, сосредоточив внимание на добавлении команд и фильтров сообщений. Вы узнаете, как реализовать команды, которые пользователи могут отправлять боту, и как обрабатывать их с помощью пакета telegram.bot.

Мы рассмотрим, как настроить обработку команд для выполнения различных действий и внедрить фильтры для управления типами сообщений. Эти инструменты позволят вашему боту стать более интерактивным и удобным для пользователей.

Эта глава предоставит вам знания и навыки, необходимые для создания более функционального и адаптивного бота, который сможет эффективно взаимодействовать с аудиторией. Надеюсь, что изучение этих возможностей вдохновит вас на создание уникальных и полезных решений.

2.1 Видео по добавлению боту поддержки команд

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, и умеет с нами здороваться.

Схематически процесс построения такого простейшего бота можно изобразить следующим образом.

  1. Создаём экземпляр класса Updater;
  2. Создаём методы, т.е. функции которые будет выполнять наш бот. В примере кода это функция say_hello(). Функции, которые вами будут использоваться как методы бота должны иметь два обязательных аргумента - bot и update, и один необязательный - args. Аргумент bot, это и есть ваш бот, с его помощью вы можете отвечать на сообщения, отправлять сообщения, или использовать любые другие доступные боту методы. Аргумент update это то, что бот получил от пользователя, по сути, то что в первой главе мы получали методом getUpdates(). Аргумент args позволяет вам обрабатывать дополнительные данные отправленные пользователем вместе с командой, к этой теме мы ещё вернёмся немного позже;
  3. Создаём обработчики, т.е. связываем какие-то действия пользователя с созданными на прошлом шаге методами. По сути обработчик это триггер, событие которое вызывает какую-то функцию бота. В нашем примере таким триггером является отправка команды /hi, и реализуется командой hi_hendler <- CommandHandler('hi', say_hello). Первый аргумент функции CommandHandler() позволяет вам задать команду, в нашем случае hi, на которую будет реагировать бот. Второй аргумент позволяет указать метод бота, мы будем вызывать метод say_hello, который будет выполняться если пользователь вызвал указанную в первом аргументе команду;
  4. Далее добавляем созданный обработчик в диспетчер нашего экземпляра класса Updater. Добавлять обработчики можно несколькими способами, в примере выше я использовал простейший, с помощью знака +, т.е. updater <- updater + hi_hendler. То же самое можно сделать с помощью метода add_handler(), который относится к классу Dispatcher, найти этот метод можно так: updater$dispatcher$add_handler();
  5. Запускаем бота с помощью команды 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 Запускаем бота в фоновом режиме

Последний шаг который нам осталось выполнить - запустить бота в фоновом режиме.

Для этого следуйте по описанному ниже алгоритму:

  1. Сохраните код бота в файл с расширением R. При работе в RStudio это делается через меню File, командой Save As….
  2. Добавьте путь к папке bin, которая в свою очередь находится в папке в которую вы установили язык R в переменную Path, инструкция тут.
  3. Создайте обычный текстовый файл, в котором пропишите 1 строку: R CMD BATCH C:\Users\Alsey\Documents\my_bot.R. Вместо *C:_bot.R* пропишите путь к своему скрипту бота. При этом важно, что бы в пути не встречалась кириллица и пробелы, т.к. это может вызвать проблемы при запуске бота. Сохраните его, и замените его расширение с txt на bat.
  4. Откройте планировщик заданий Windows, есть множество способов это сделать, например откройте любую папку и в адресс введите %windir%\system32\taskschd.msc /s. Другие способы запуска можно найти тут.
  5. В верхнем правом меню планировщика нажмите “Создать задачу…”.
  6. На вкладке “Общие” задайте произвольное имя вашей задаче, и переключатель перевидите в состояние “Выполнять для всех пользователей”.
  7. Перейдите на вкладку “Действия”, нажмите “Создать”. В поле “Программа или сценарий” нажмите “Обзор”, найдите созданный на втором шаге bat файл, и нажмите ОК.
  8. Жмём ОК, при необходимости вводим пароль от вашей учётной записи операционной системы.
  9. Находим в планировщике созданную задачу, выделяем и в нижнем правом углу жмём кнопку “Выполнить”.

Наш бот запущен в фоновом режиме, и будет работать до тех пор, пока вы не остановите задачу, или не выключите ваш ПК или сервер на котором его запустили.

2.8 Обработка голосовых сообщений. Переводим голосовое сообщение в текст

давайте разберём ещё один довольно полезный пример, напишем бота, который будет принимать голосовые сообщение, при чём эти голосовые сообщения можно пересылать из любого другого чата, а на выходе давать вам его текстовую расшифровку.

2.8.1 Функция для преобразования голоса в текст

Для начала нам необходимо разработать основу, т.е. функцию, которая на вход получит аудио файл, и преобразует его в текст. Для этого можно использовать Google Speech-to-Text API. Это условно бесплатный сервис, бесплатно вы можете в месяц конвертировать до 60 минут аудиоо в текст. Этот лимит в будущем возможно будет изменён.

Прежде всего, нам нужно настроить проект в Google Cloud:

  1. Зайдите на console.cloud.google.com и создайте новый проект.
  2. Включите API Speech-to-Text в разделе “APIs & Services”.
  3. Создайте учетные данные (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 Тесты

Для закрепления материла рекомендую вам пройти тест доступный по ссылке.

2.12.2 Задания

  1. Создайте бота, который будет по команде /sum и переданное в качестве дополнительных параметров произвольное количество перечисленных через пробел чисел, возвращать их сумму.
Если вы всё сделали правильно результат должен быть таким: