Статьи


Telegram-бот на Kotlin и Spring Boot

Вернуться назад Исходники

6 октября 2020

Тэги: Spring Spring Boot Kotlin gradle

В настоящее время чат-боты в Telegram не делал только ленивый. Они плотно вошли в нашу жизнь и почти у каждой компании есть бот, решающий какие-то бизнесовые задачи, тем самым разгружая «живых» сотрудников. После прочтения этой статьи вы сможете создать и запустить свой чат-бот в Telegram. Также пример готового приложения доступен по ссылке на github.

Регистрация бота в Telegram

Сперва нам нужно выбрать подходящее имя для бота и зарегистрировать его в Telegram. Регистрация нового бота происходит через бота по имени BotFather. Просто найдите его через поиск контактов Telegram. В чате вы всегда можете понять, что общаетесь с ботом, т.к. рядом с его именем есть подпись «bot». BotFather позволяет управлять вашими ботами в диалоговом режиме. Команды боту представляют собой текст, начинающийся со слеша.

Для создания нового бота отправьте команду /newbot. Вам будет предложено ввести имя бота. На данном шаге постарайтесь не использовать слово «bot» в названии. Если выбранное вами имя не занято, то далее вам будет предложено ввести логин для этого бота. Причём он должен заканчиваться на «bot». Если логин не занят, то вам будет сгенерирован access token для работы с Telegram API по http. Сохраните этот токен - он нам понадобится далее.

Создание бота на Spring Boot

За основу нашего чат-бота возьмём Spring Boot. Код будем писать на Kotlin. Воспользуем сайтом start.spring.io для создания заготовки нашего приложения. В настройках выберем Gradle Project и Kotlin, в качестве зависимости нам здесь будет достаточно только Web. Скачаем заготовку проекта и откроем файл build.gradle.kts. Проверьте, что в секции dependencies присутствует org.springframework.boot:spring-boot-starter-web. Также добавим туда библиотеку для работы с Telegram org.telegram:telegrambots-spring-boot-starter:4.1.

dependencies {
    implementation("org.springframework.boot:spring-boot-starter-web")
    implementation("org.telegram:telegrambots-spring-boot-starter:4.1")
    // другие зависимости
}

Теперь давайте найдём главный класс нашего приложения, в котором находится метод main() и аннотация @SpringBootApplication. В него нужно добавить инициализацию контекста Telegram API:

@SpringBootApplication
class BotApplication

fun main(args: Array<String>) {
    ApiContextInitializer.init() // инициализация контекста для Telegram API
    runApplication<BotApplication>(*args)
}

Если этого не сделать, то ошибок при запуске не будет, но и бот также работать не будет.

Теперь создадим новый сервис. В telegram боты можно подключать двумя способами: long polling и web-hook. В случае с long polling наше приложение кидает запрос и ждёт ответа от сервера telegram. Сервер ответит не сразу, а только тогда, когда произойдёт какое-либо событие (например, сообщение от пользователя). А в случае с webhook сервер telegram сам будет дёргать заранее зарегистрированные эндпоинты нашего приложения. В подключенной нами библиотеке поддерживаются оба варианта, но webhook чуть сложнее в настройке. Поэтому рассмотрим long polling.

Унаследуем наш сервис от класса TelegramLongPollingBot. Этот абстрактный класс потребует от нас реализации методов getBotUsername(), getBotToken() и onUpdateReceived().

Первые два метода должны возвращать те самые данные, которые мы получили при регистрации. Однако нельзя их хардкодить в виде констант. Они должны подгружаться из параметров приложения.

@Service
class DevmarkBot : TelegramLongPollingBot() {

    @Value("\${telegram.botName}")
    private val botName: String = ""

    @Value("\${telegram.token}")
    private val token: String = ""

    override fun getBotUsername(): String = botName

    override fun getBotToken(): String = token

Благодаря аннотации @Value Spring сам подставит параметры из файла application.properties. Пропишем их в этом файле, а лучше сразу его переименуем в application.yml, чтобы писать в yaml-формате:

telegram:
  botName: devmark_ru_bot
  token: ${TOKEN}

Здесь мы указываем имя бота (telegram.botName) в явном виде, а вот токен (telegram.token) подгружаем из переменной окружения, т.к. этот токен должен сохраняться в секрете. Переменную окружения можно указывать при запуске приложения из командной строки через опцию -D или непосредственно в Idea.

Обработка запросов от пользователя

Теперь вернёмся к нашему сервису и реализуем метод onUpdateReceived().

override fun onUpdateReceived(update: Update) {
    if (update.hasMessage()) {
        val message = update.message
        val chatId = message.chatId
        val responseText = if (message.hasText()) {
            val messageText = message.text
            when {
                messageText == "/start" -> "Добро пожаловать!"
                else -> "Вы написали: *$messageText*"
            }
        } else {
            "Я понимаю только текст"
        }
        sendNotification(chatId, responseText)
    }
}

В начале мы проверяем объект типа Update на наличие сообщения с помощью метода hasMessage(). Далее, извлекаем chatId (уникальный идентификатор пользователя в telegram). Затем проверяем, что входящее сообщение содержит текст (а не стикер, к примеру). Если сообщение от пользователя равно строке «/start», то мы приветствуем пользователя. Дело в том, что именно такое сообщение отправляется, когда вы впервые подключаетесь к боту и жмёте кнопку «Start». Для любого другого текста мы просто дублируем его в ответе. Если же текста нет, бот ответит, что понимает только текст.

Отправка сообщения происходит во вспомогательном методе sendNotification().

private fun sendNotification(chatId: Long, responseText: String) {
    val responseMessage = SendMessage(chatId, responseText)
    responseMessage.setParseMode("Markdown")
    execute(responseMessage)
}

В нём мы создаём объект ответа, заполняя chatId и текст ответа. Затем с помощью метода setParseMode() включаем режим разметки Markdown. Этот режим позволяет делать простое форматирование текста. Например, текст, обрамлённый с двух сторон символами звёздочки будет отображаться жирным. Затем для отправки сообщения вызываем метод execute() из родительского класса.

Добавляем кнопки

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

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

private fun sendNotification(chatId: Long, responseText: String) {
    val responseMessage = SendMessage(chatId, responseText)
    responseMessage.setParseMode("Markdown")
    // добавляем 4 кнопки
    responseMessage.replyMarkup = getReplyMarkup(
            listOf(
                    listOf("Кнопка 1", "Кнопка 2"),
                    listOf("Кнопка 3", "Кнопка 4")
            )
    )
    execute(responseMessage)
}

Тут мы просто указываем свойство replyMarkup, для чего вызываем метод getReplyMarkup(), передавая ему на вход список списков строк. Сам метод выглядит примерно так:

private fun getReplyMarkup(allButtons: List<List<String>>): ReplyKeyboardMarkup {
    val markup = ReplyKeyboardMarkup()
    markup.keyboard = allButtons.map { rowButtons ->
        val row = KeyboardRow()
        rowButtons.forEach { rowButton -> row.add(rowButton) }
        row
    }
    return markup
}

Создаём объект ReplyKeyboardMarkup. Как нетрудно догадаться по названию, он отвечает за разметку кнопок. Затем проходимся по каждой строке, создавая KeyboardRow и заполняем её кнопками. Для создания кнопки требуется указать только текст.

Наконец, немного модифицируем наш обработчик запросов:

when {
    messageText == "/start" -> "Добро пожаловать!"
    messageText.startsWith("Кнопка ") -> "Вы нажали кнопку" // обработка нажатия кнопки
    else -> "Вы написали: *$messageText*"
}

Как только пользователь отправит сообщение, начинающееся со строки «Кнопка », мы определим, что пользователь нажал именно кнопку.

Заключение

Как видите, Spring Boot позволяет запустить чат-бот с минимальным количеством усилий. Вам же остаётся только реализовать саму обработку сообщения согласно вашей бизнес-логике.

Подавляющее большинство чат-ботов работает именно с текстовой информацией. Помимо применения в бизнесе также известно немало успешных примеров реализации текстовых игр и даже MMORPG. Именно поэтому чат-боты Telegram сейчас используются повсеместно.