Статьи
Утилиты Telegram YouTube Отзывы

Telegram-бот на Spring Boot

Видеогайд Исходники

27 декабря 2023

Тэги: gradle, Kotlin, Spring Boot, yaml, YouTube, руководство.

Содержание

  1. Регистрация бота в Telegram
  2. Создание бота на Spring Boot
  3. Конфигурация обработчика
  4. Обработка запросов от пользователя
  5. Обработка команд
  6. Команды с параметрами
  7. Добавляем кнопки
  8. Выводы

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

Telegram-бот на Kotlin и Spring Boot

Пример готового приложения можно найти на github.

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

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

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

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

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

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

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

Унаследуем наш класс от класса TelegramLongPollingBot. Этот абстрактный класс потребует от нас реализации методов getBotUsername(), и processNonCommandUpdate(). Также в конструкторе базовому классу следует передать токен, который мы получили при регистрации бота. Однако его нельзя хардкодить в виде константы. Он должен подгружаться из конфига.

@Component
class DevmarkBot(
    @Value("\${telegram.token}")
    token: String,
) : TelegramLongPollingCommandBot(token) {

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

    override fun getBotUsername(): String = botName

    override fun processNonCommandUpdate(update: Update) {
    }
}

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

telegram:
  botName: devmark_ru_test_bot
  token: ${TELEGRAM_TOKEN}

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

Конфигурация обработчика

Чтобы созданный нами сервис обрабатывал сообщения, его надо зарегистрировать (только для Spring Boot 3). Создадим новую конфигурацию BotConfig:

@Configuration
class BotConfig {
    @Bean
    fun telegramBotsApi(bot: DevmarkBot): TelegramBotsApi =
        TelegramBotsApi(DefaultBotSession::class.java).apply {
            registerBot(bot)
        }
}

В ней создаём новый бин с помощью аннотации @Bean, в которую в качестве аргумента передаём наш обработчик DevmarkBot. Spring автоматически найдёт экземпляр этого класса и подставит его сюда при старте приложения. Внутри мы с помощью класса TelegramBotsApi и метода registerBot() регистрируем наш обработчик. Кстати, вы можете зарегистрировать здесь несколько обработчиков и в рамках одного приложения у вас будут функционировать сразу несколько ботов Telegram.

Если в Spring Boot 3 не создать такую конфигурацию, то приложение будет запускаться без ошибок, однако бот работать не будет.

Если же вы используете Spring Boot 2, то конфигурационный бин вообще не нужно создавать. Если вы его создадите, то приложение при запуске будет сообщать о том, что уже запущен другой экземпляр бота.

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

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

override fun processNonCommandUpdate(update: Update) {
    if (update.hasMessage()) {
        val chatId = update.message.chatId.toString()
        if (update.message.hasText()) {
            execute(createMessage(chatId, "Вы написали: *${update.message.text}*"))
        } else {
            execute(createMessage(chatId, "Я понимаю только текст!"))
        }
    }
}

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

Отправка сообщения происходит с помощью метода execute() из базового класса. Само сообщение формируем с помощью утилитарного метода createMessage().

fun createMessage(chatId: String, text: String) =
    SendMessage(chatId, text)
        .apply { enableMarkdown(true) }
        .apply { disableWebPagePreview() }

В нём мы создаём объект SendMessage с указанием текста и chatId. Также включаем поддержку форматирования текста с помощью markdown и запрещаем отображение превью для внешних ссылок, если таковые будут (просто чтобы не загромождать сообщение).

Обработка команд

Наш бот уже умеет зеркально отвечать на любой запрос пользователя. Но этого мало. Любой telegram-бот должен поддерживать обработку команд. Командой называется любой текст, начинающийся со слеша. И как минимум одну команду мы должны обязательно обрабатывать, т.к. она отправляется при первом взаимодействии пользователя с ботом. Эта команда /start.

Благодаря библиотеке telegrambotsextensions, которую мы добавили в проект, для обработки нам не нужно в явном виде обрабатывать входящее сообщение пользователя, проверять его на наличие слеша и т.п. Достаточно просто создать класс StartCommand, унаследовав его от BotCommand.

@Component
class StartCommand : BotCommand(CommandName.START.text, "") {

    override fun execute(absSender: AbsSender, user: User, chat: Chat, arguments: Array<out String>) {
        absSender.execute(createMessage(chat.id.toString(), "Добро пожаловать!"))
    }
}

Определяем его как спринговый компонент и передаём в конструктор базового класса название команды. Все эти названия удобно хранить в отдельном enum.

enum class CommandName(val text: String) {
    START("start"),
}

Во второй параметр конструктора передаётся description команды, но он ни на что не влияет, поэтому я передаю пустую строку.

Внутри метода execute() наш обработчик просто приветствует пользователя, отправляя сообщение через AbsSender. На самом деле это базовый класс нашего DevmarkBot.

Чтобы все команды, которые мы создадим, автоматически появлялись в боте, их надо зарегистрировать. Для этого внесём правки в DevmarkBot.

@Component
class DevmarkBot(
    commands: Set<BotCommand>,
    @Value("\${telegram.token}")
    token: String,
) : TelegramLongPollingCommandBot(token) {

    init {
        registerAll(*commands.toTypedArray())
    }
    // ...
}

Здесь Spring сам найдёт все реализации класса BotCommand, которые помечены как компоненты, и подставит их в конструктор. Затем мы их зарегистрируем с помощью метода registerAll() в секции init.

Команда start в telegram-боте

Теперь можно запустить наше приложение и выполнить первый запрос к боту. Если всё сделано правильно, вы увидите кнопку Start и при нажатии на неё получите приветствие.

Команды с параметрами

Команда /start является самой простой, т.к. не содержит параметров. Но вы можете передавать любые параметры в команду, разделяя их пробелами.

Давайте создадим новую команду /sum, которая на вход будет получать целые числа и возвращать их сумму. Количество параметров в данном случае может быть любым.

@Component
class SumCommand : BotCommand(CommandName.SUM.text, "")  {
    override fun execute(absSender: AbsSender, user: User, chat: Chat, arguments: Array<out String>) {
        val numbers = arguments.map { it.toInt() }
        val sum = numbers.sum()
        absSender.execute(
            createMessage(
                chat.id.toString(),
                numbers.joinToString(separator = " + ", postfix = " = $sum"),
            )
        )
    }
}

Все параметры, которые получит SumCommand, содержатся в arguments. Преобразуем их в целые числа с помощью map() и toInt(). Затем суммируем их с помощью sum(). Наконец, формируем ответ с помощью метода joinToString(), в котором перечислим через + все полученные аргументы-числа и после = выведем итоговую сумму.

Команда с параметрами в telegram-боте

Больше ничего делать не нужно. Запускаем бота и выполним команду «/sum 10 20 30». В ответ бот вернёт «10 + 20 + 30 = 60».

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

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

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

@Component
class ButtonsCommand : BotCommand(CommandName.BUTTONS.text, "") {
    override fun execute(absSender: AbsSender, user: User, chat: Chat, arguments: Array<out String>) {
        absSender.execute(
            createMessageWithSimpleButtons(
                chat.id.toString(),
                "Нажмите на одну из кнопок.",
                listOf(
                    listOf("Кнопка 1", "Кнопка 2"),
                    listOf("Кнопка 3", "Кнопка 4"),
                )
            )
        )
    }
}

Внутри используем утилитарный метод createMessageWithSimpleButtons():

fun createMessageWithSimpleButtons(chatId: String, text: String, simpleButtons: List<List<String>>) =
    createMessage(chatId, text)
        .apply {
            replyMarkup = getSimpleKeyboard(simpleButtons)
        }

fun getSimpleKeyboard(allButtons: List<List<String>>): ReplyKeyboard =
    ReplyKeyboardMarkup().apply {
        keyboard = allButtons.map { rowButtons ->
            val row = KeyboardRow()
            rowButtons.forEach { rowButton -> row.add(rowButton) }
            row
        }
        oneTimeKeyboard = true
    }

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

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

Теперь запустим бота, выполним команду /buttons, чтобы увидеть кнопки.

Простые кнопки в telegram-боте

Нажмём на одну из них и бот нам повторит надпись на этой кнопке «Вы написали: Кнопка 2». Ещё раз повторюсь, что такие кнопки не более чем предустановленные сообщения для пользователя. Они не хранят никакого дополнительного контекста.

Выводы

Мы узнали как зарегистрировать собственного бота в telegram и создали простую заготовку для него на Kotlin и Spring Boot. Мы подробно рассмотрели как создавать обработчики для простых команд типа /start, которые должны быть в каждом боте. Также узнали как передавать и обрабатывать параметры в командах. Наконец, рассмотрели, как отображать простые кнопки в клиентском приложении Telegram.

В следующей статье Inline-кнопки в telegram-боте мы добавим в наш бот больше интерактива с помощью inline-кнопок и обработчиков обратного вызова.



Комментарии

10.07.2022 19:40 Данил

круто! ждём продолжения темы, про webhook очень интересно

02.10.2022 17:44 Димон

Я не понял, а как зупускать бота?

02.10.2022 21:53 devmark

Просто запускаем приложение. Либо из вашей среды разработки (для тестирования), либо "java -jar имя_файла" из консоли. Если в конфигурационных файлах правильно указан токен и имя бота, то ваш бот начнёт принимать сообщения.

01.02.2023 16:23 Ринат

Добрый день. Подскажите как добавить кнопки в сообщение? Т.е к примеру пользователь нажал на кнопку 1 а ему приходит в сообщение к примеру для выбора 3 кнопки. И что бы кнопки были по размеру самого большого текста в них
Спасибо!

04.02.2023 01:18 devmark

Набор кнопок вы можете формировать в каждом ответном сообщении (см. метод sendNotification()). Чтобы разместить три кнопки в ряд, вам нужно добавить их в первом внутреннем списке (где в примере кнопки 1 и 2). А второй внутренний список можно удалить.

Что касается размеров кнопок - это зависит от конкретного telegram-клиента и все кнопки автоматически отображаются одинакового размера.

02.03.2023 16:26 Draccul

Объясните поподробнее. как вынести бота из среды разработки, что бы запускать как программу на компьютере без лишнего по

02.03.2023 16:39 devmark

Так же, как и любое другое приложение на Java. Сначала собираем его из командной строки (если у вас gradle):
./gradlew clean build
затем запускаем
java -jar build/libs/имя_проекта.jar

03.03.2023 08:28 Draccul

А как должно выглядеть описание в Манифесте, иначе он не запускается

03.03.2023 15:14 devmark

Какой манифест имеется в виду? Вручную ничего прописывать не надо. Также не забудьте, что для запуска бота требуется установить переменную окружения TOKEN, в которую прописать токен доступа к вашему боту в Telegram.

04.03.2023 09:11 Draccul

Выдает при запуске вот такую ошибку, запускаю через командную строку...
Exception in thread "main" java.lang.NoClassDefFoundError: kotlin/jvm/internal/Intrinsics
        at MainKt.main(Main.kt)
Caused by: java.lang.ClassNotFoundException: kotlin.jvm.internal.Intrinsics
        at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:641)
        at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:188)
        at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:521)
        ... 1 more

04.03.2023 19:17 devmark

Скорее всего, у вас некорректный build.gradle.kts. Можно сгенерить его заново через сайт start.spring.io. Вам там нужно выбрать Gradle (Kotlin dsl), kotlin. Из зависимостей можно сразу добавить Spring Web, а затем вручную добавить зависимость telegram, как написано в статье.

08.03.2023 21:31 vitnine

Библиотека поддерживает корутины?

14.03.2023 00:34 devmark

Библиотека поддерживает как синхронное, так и асинхронное взаимодействие.

Я обновил статью и проект с учётом последних версий библиотек на текущий момент. Если у кого-то пример не запускается, попробуйте последнюю версию из гитхаба.

02.04.2023 20:59 Ринат

Подскажите как отправлять файлы пользователю? К примеру нужно что бы при нажатии кнопки бот отправлял файл пользователю?
Спасибо!

26.07.2023 23:17 devmark

Ещё раз актуализировал гайд: добавил раздел про конфигурационный бин и перевёл проект на гитхабе на Spring Boot 3.

01.08.2023 02:29 Иван

Добрый день!

Скачал проект, подставил токен и имя бота, но он не работает. В консоли выдает это:

2023-08-01T01:25:17.769+02:00 INFO 3900 --- [         main] ru.devmark.bot.BotApplicationKt         : Starting BotApplicationKt using Java 18.0.1.1 with PID 3900 (C:\Users\Tardis\Downloads\devmark-ru-bot-main\devmark-ru-bot-main\build\classes\kotlin\main started by Tardis in C:\Users\Tardis\Downloads\devmark-ru-bot-main)
2023-08-01T01:25:17.772+02:00 INFO 3900 --- [         main] ru.devmark.bot.BotApplicationKt         : No active profile set, falling back to 1 default profile: "default"

01.08.2023 02:40 Иван

.BeanCreationException: Error creating bean with name 'telegramBotsApi' defined in class path resource [ru/devmark/bot/config/BotConfig.class]: Failed to instantiate [org.telegram.telegrambots.meta.TelegramBotsApi]: Factory method 'telegramBotsApi' threw exception with message: Error removing old webhook
2023-08-01T01:25:19.421+02:00 INFO 3900 --- [         main] o.apache.catalina.core.StandardService : Stopping service [Tomcat]
2023-08-01T01:25:19.431+02:00 INFO 3900 --- [         main] .s.b.a.l.ConditionEvaluationReportLogger :

Error starting ApplicationContext. To display the condition evaluation report re-run your application with 'debug' enabled.
2023-08-01T01:25:19.447+02:00 ERROR 3900 --- [         main] o.s.boot.SpringApplication             : Application run failed

01.08.2023 02:40 Иван

org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'telegramBotsApi' defined in class path resource [ru/devmark/bot/config/BotConfig.class]: Failed to instantiate [org.telegram.telegrambots.meta.TelegramBotsApi]: Factory method 'telegramBotsApi' threw exception with message: Error removing old webhook
at org.springframework.beans.factory.support.ConstructorResolver.instantiate(ConstructorResolver.java:659) ~[spring-beans-6.0.11.jar:6.0.11]
at org.springframework.beans.factory.support.ConstructorResolver.instantiateUsingFactoryMethod(ConstructorResolver.java:647) ~[spring-beans-6.0.11.jar:6.0.11]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.instantiateUsingFactoryMethod(AbstractAutowireCapableBeanFactory.java:1332) ~[spring-beans-6.0.11.jar:6.0.11]
at

07.08.2023 10:40 devmark

В ошибке упоминается webhook, однако пример использует long polling. Возможно, для вашего бота настроен другой метод взаимодействия с Telegram.

28.02.2024 21:04 Анатолий

У меня пока не очень большой опыт в IntelliJ IDEA, так что извините за дилетантские вопросы
Вот мои вопросы пока не начинал редактирование, а только сгенерил:
1) В видео ничего не сказано про (Конфигурация обработчика) - где это и как создается
2) По поводу переменной окружения (TELEGRAM_TOKEN) - где ее создавать и как
3) у меня в папках нет build.gradle.kts, есть только build.gradle

29.02.2024 17:52 devmark

1) видео немного устарело, см. в данной статье раздел про конфигурацию обработчика
2) в Idea в Run Configurations можно задать любые переменные окружения (Environment variables)
3) это значит вы при генерации выбрали просто gradle (скрипт сборки на groovy). а надо именно gradle - kotlin (скрипт сборки на kotlin). Хотя разница между ними не столь уж большая, удобно для kotlin-проектов использовать именно kotlin-синтаксис.

Добавить комментарий

×

devmark.ru