Разработка телеграм бота на Kotlin + Spring Boot

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Привет, читателям Хабра!


В этой статье я расскажу о том, как быстро и легко разработать свой собственный телеграм бот на языке Kotlin с использованием Spring Boot.


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


Технологии


Мой выбор пал на следующий стек технологий:


  • Kotlin
  • Spring Boot 2.5+
  • JOOQ
  • Freemarker
  • PostgreSQL
  • org.telegram.telegrambots

Обоснования подборки технологий


Spring Boot и весь Spring Framework в JVM мире стал неотъемлимой частью создания больших и сложных энтерпрайз систем. Пал выбор именно на него, так как порой хочется сделать не просто бота, а полноценное приложение, которым будет удобно пользоваться и удобно масштабировать.
Kotlin считается неким витком развития в мире JVM, он проще JAVA и очень хорошо интегрирован в Spring Framework
JOOQ — механизим, который помогает на DSL подобном языке формировать sql запросы.
Freemarker — шаблонизатор, необходим для формирования динамичных текстовок
PostgreSQL — СУБД. Тут субъективный выбор. Считаю его лучшим из бесплатных инструментов.
org.telegram.telegrambots — набор библиотек для Telegram Api


Источники


Сам код лежит в гитхабе.
Как создать нового бота и описание api можно найти тут


Руководство


Как и в любое приложении в JVM мире, начнем работу с описанием зависимостей.


import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
import nu.studer.gradle.jooq.JooqEdition
import nu.studer.gradle.jooq.JooqGenerate

val postgresVersion = "42.3.1"
val telegramBotVersion = "5.3.0"

// Список необходимых плагинов
plugins {
    id("nu.studer.jooq") version("6.0.1")
    id("org.flywaydb.flyway") version("7.7.0")
    id("org.springframework.boot") version "2.5.6"
    id("io.spring.dependency-management") version "1.0.11.RELEASE"
    kotlin("jvm") version "1.5.31"
    kotlin("plugin.spring") version "1.5.31"
}

group = "ru.template.telegram.bot.kotlin"
version = "0.0.1-SNAPSHOT"
java.sourceCompatibility = JavaVersion.VERSION_11

repositories {
    mavenCentral()
}

// механизм, который поможет сгенерить метаданные
configurations {
    compileOnly {
        extendsFrom(configurations.annotationProcessor.get())
    }
}

tasks.clean {
    delete("src/main/java")
}

extra["springCloudVersion"] = "2020.0.4"

val flywayMigration = configurations.create("flywayMigration")

// Надстройка для миграции данных в СУБД
flyway {
    validateOnMigrate = false
    configurations = arrayOf("flywayMigration")
    url = "jdbc:postgresql://localhost:5432/kotlin_template"
    user = "postgres"
    password = "postgres"
}

// список зависимстей
dependencies {
    flywayMigration("org.postgresql:postgresql:$postgresVersion")
    jooqGenerator("org.postgresql:postgresql:$postgresVersion")
    runtimeOnly("org.postgresql:postgresql")

   //Классические стартеры spring boot
    implementation("org.springframework.boot:spring-boot-starter")
    implementation("org.springframework.boot:spring-boot-starter-jooq")
    implementation("org.springframework.boot:spring-boot-starter-freemarker")
    implementation("org.springframework.boot:spring-boot-starter-web")

    implementation("org.telegram:telegrambots:$telegramBotVersion")
    implementation("org.telegram:telegrambotsextensions:$telegramBotVersion")
    implementation("org.telegram:telegrambots-spring-boot-starter:$telegramBotVersion")

    // зависимости, которые помогут сгенерить метаданные
    compileOnly("org.springframework.boot:spring-boot-configuration-processor")
    annotationProcessor("org.springframework.boot:spring-boot-configuration-processor")

    implementation("org.jetbrains.kotlin:kotlin-reflect")
    implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
    testImplementation("org.springframework.boot:spring-boot-starter-test")
}

dependencyManagement {
    imports {
        mavenBom("org.springframework.cloud:spring-cloud-dependencies:${property("springCloudVersion")}")
    }
}

tasks.withType<KotlinCompile> {
    kotlinOptions {
        freeCompilerArgs = listOf("-Xjsr305=strict")
        jvmTarget = "11"
    }
}

tasks.withType<Test> {
    useJUnitPlatform()
}

// Настройка для JOOQ, в которой описано правило формирования POJO классов для формирования запросов при помощи DSL кода
jooq {
    edition.set(JooqEdition.OSS)

    configurations {
        create("main") {
            jooqConfiguration.apply {
                jdbc.apply {
                    driver = "org.postgresql.Driver"
                    url = flyway.url
                    user = flyway.user
                    password = flyway.password
                }
                generator.apply {
                    name = "org.jooq.codegen.DefaultGenerator"
                    generate.apply {
                        isDeprecated = false
                        isRecords = true
                        isImmutablePojos = false
                        isFluentSetters = false
                        isJavaBeansGettersAndSetters = false
                        isSerializablePojos = true
                        isVarargSetters = false
                        isPojos = true
                        isNonnullAnnotation = true
                        isUdts = false
                        isRoutines = false
                        isIndexes = false
                        isRelations = true
                        isPojosEqualsAndHashCode = true
                    }
                    database.apply {
                        name = "org.jooq.meta.postgres.PostgresDatabase"
                        inputSchema = "public"
                        excludes = "flyway_schema_history"
                    }
                    target.apply {
                        // Пакет куда отрпавляются сгенерированные классы
                        packageName = "ru.template.telegram.bot.kotlin.template.domain"
                        directory = "src/main/java"
                    }
                    strategy.name = "org.jooq.codegen.DefaultGeneratorStrategy"
                }
            }
        }
    }

    // таска для генерации JOOQ классов
    tasks.named<JooqGenerate>("generateJooq").configure {
        inputs.files(fileTree("src/main/resources/db/migration"))
            .withPropertyName("migrations")
            .withPathSensitivity(PathSensitivity.RELATIVE)
        allInputsDeclared.set(true)
        outputs.upToDateWhen { false }
    }
}

Как мы видим из описания кода выше, мы собираем зависимости при помощи Gradle. Не будем подробно останавливаться на теме: как правильно написать gradle файл. В интернете много примеров. Сейчас нам не так это важно.


Следующим этапом — создание главного класса, который будет запускать нашего бота.


package ru.template.telegram.bot.kotlin.template

import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication

// Все Spring Boot приложения начинаются с аннотации @SpringBootApplication
@SpringBootApplication
class Application

fun main(args: Array<String>) {
    runApplication<Application>(*args)
}

Для примера, весь наш код описан в пакете ru.template.telegram.bot.kotlin.template. Там будут лежать и прочие компоненты нашей архитектуры.


Создадим пакеты:


  • api — Классы, которые относятся к непосредственному взаимодействию с Телеграм API (отправка и получение данных)
  • command — Список команд телеграм бота
  • component — Прочие бины.
  • config — Конфигурация приложения
  • dto — DTO классы
  • enums — Енумы
  • event — Список классов для формирования ивентов Application Publisher
  • listener — Приём событий Application Publisher
  • properties — Классы настроек приложения. Сами настройки лежат в ресурсах приложения (appication.yml)
  • repository — Слой взаимодействия с СУБД
  • service — Сервисы приложения
  • strategy — Стратегии. Это те компоненты, которые нужно менять, добавлять и удалять по ходу изменения бизнес процессов

Начало формирование архитектуры с небольшим примером


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


Создадим файл application.yml


# Настройка для телеграм апи
bot:
  username: kotlin_template_bot
  token: [your bot token here]
# Настройка для СУБД
spring:
  datasource:
    url: jdbc:postgresql://localhost:5432/kotlin_template
    username: postgres
    password: postgres

После чего опишем нашего бота в виде класса


package ru.template.telegram.bot.kotlin.template.properties

import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.stereotype.Component

@Component
@ConfigurationProperties(prefix = "bot")
// Всё, что есть в application.yml можно описать в виде класса. Делается в первую очередь для удобства
data class BotProperty(
    var username: String = "",
    var token: String = ""
)

В примере представлены несколько команд бота. Они описаны в виде енумов


package ru.template.telegram.bot.kotlin.template.enums

// Енум состоит из самой команды, команды бота в телеграме и ее словестное описание
enum class CommandCode(val command: String, val desc: String) {
    START("start", "Start work"),
    USER_INFO("user_info", "user info"),
    BUTTON("button", "button yes no")
}

Для примера реализуем несколько комманд: Начало работы (приветствие), информация о пользователе ( на этом этапе просто обновим данные в базе случайным текстом) и сообщение с кнопками (здесь нажмём на кнопку из предложенных)


Для простоты у нас будет 1 таблица users


Создадим миграцию в resources/db/migration


create table users
(
    id        int8 primary key not null,
    step_code varchar(100), -- код этапа
    text      varchar(100), -- произвольный текст
    accept    varchar(3) -- данные из кнопок
);

Теперь необходимо подготовить UsersRepository в котором будут реализованы все методы, которые понадобятся нам в работе.


package ru.template.telegram.bot.kotlin.template.repository

import org.jooq.DSLContext
import org.springframework.stereotype.Repository
import ru.template.telegram.bot.kotlin.template.domain.Tables
import ru.template.telegram.bot.kotlin.template.domain.tables.pojos.Users
import ru.template.telegram.bot.kotlin.template.enums.StepCode

@Repository
class UsersRepository(private val dslContext: DSLContext) {

    private val users = Tables.USERS

    // Проверка на существование пользователя в базе. Нужно 1 раз для команды /start
    fun isUserExist(chatId: Long): Boolean {
        return dslContext.selectCount().from(users).where(users.ID.eq(chatId)).fetchOneInto(Int::class.java) == 1
    }

   // Созадние пользователя для команды /start
    fun createUser(chatId: Long): Users {
        val record = dslContext.newRecord(users, Users().apply {
            id = chatId
            stepCode = StepCode.START.toString()
        })
        record.store()
        return record.into(Users::class.java)
    }

    // получить информацию о пользователе
    fun getUser(chatId: Long) =
        dslContext.selectFrom(users).where(users.ID.eq(chatId)).fetchOneInto(Users::class.java)

    // Обновление этапа в боте
    fun updateUserStep(chatId: Long, stepCode: StepCode): Users =
        dslContext.update(users)
            .set(users.STEP_CODE, stepCode.toString())
            .where(users.ID.eq(chatId)).returning().fetchOne()!!.into(Users::class.java)

    // Обновление текста. Этот метод срабатывает у команды /user_info
    fun updateText(chatId: Long, text: String) {
        dslContext.update(users)
            .set(users.TEXT, text)
            .where(users.ID.eq(chatId)).execute()
    }

    // Обновление данных пришедшие от кнопок в команде /button
    fun updateAccept(chatId: Long, accept: String) {
        dslContext.update(users)
            .set(users.ACCEPT, accept)
            .where(users.ID.eq(chatId)).execute()
    }
}

Здесь уже можно и нужно сформировать POJO объекты при помощи таски gradle flywayMigrate generateJooq


Создать новую команду тоже для нас не проблема. В данной статье опишем только одну команду, всё остальное есть в исходниках. Делается по аналогии


package ru.template.telegram.bot.kotlin.template.command

import org.springframework.context.ApplicationEventPublisher
import org.springframework.stereotype.Component
import org.telegram.telegrambots.extensions.bots.commandbot.commands.BotCommand
import org.telegram.telegrambots.meta.api.objects.Chat
import org.telegram.telegrambots.meta.api.objects.User
import org.telegram.telegrambots.meta.bots.AbsSender
import ru.template.telegram.bot.kotlin.template.enums.CommandCode
import ru.template.telegram.bot.kotlin.template.enums.StepCode
import ru.template.telegram.bot.kotlin.template.event.TelegramStepMessageEvent
import ru.template.telegram.bot.kotlin.template.repository.UsersRepository

@Component
class StartCommand(
    private val usersRepository: UsersRepository,
    private val applicationEventPublisher: ApplicationEventPublisher // Интерфейс который отправляет событие 
) : BotCommand(CommandCode.START.command, CommandCode.START.desc) {

    companion object {
        private val START_CODE = StepCode.START
    }

    override fun execute(absSender: AbsSender, user: User, chat: Chat, arguments: Array<out String>) {
        val chatId = chat.id // chatId передает телеграм

       // если пользователя в базе не существует, то создаём его, иначе обновляешь этап
        if (usersRepository.isUserExist(chatId)) {
            usersRepository.updateUserStep(chatId, START_CODE)
        } else usersRepository.createUser(chatId)

        applicationEventPublisher.publishEvent(
            TelegramStepMessageEvent(chatId = chatId, stepCode = START_CODE)
        )
    }

}

Как мы видим из кода, начинаем формировать событие TelegramStepMessageEvent.
BotCommand — это интерфейс описания команд телеграм АПИ


Класс TelegramStepMessageEvent лежит в пакете event


package ru.template.telegram.bot.kotlin.template.event

import ru.template.telegram.bot.kotlin.template.enums.StepCode

class TelegramStepMessageEvent(
    // chatId из бота
    val chatId: Long,
    // Этап или шаг в боте (стартовый, выбор кнопки, сообщение пришедшее после кнопки и тд и тп). Не путать с командами, так как в команде может быть несколько этапов
    val stepCode: StepCode
)

StepCode — enum, который носит информацию о типе сообщения, о шаге и прочую системную информацию


package ru.template.telegram.bot.kotlin.template.enums

// Тип (Простой текст или текст с кнопками) и botPause - остановить переход на новый этап для принятия решения пользователем
enum class StepCode(val type: StepType, val botPause: Boolean) {
    START(StepType.SIMPLE_TEXT, false),
    USER_INFO(StepType.SIMPLE_TEXT, true),
    BUTTON_REQUEST(StepType.INLINE_KEYBOARD_MARKUP, true),
    BUTTON_RESPONSE(StepType.SIMPLE_TEXT, true)
}

enum class StepType {
    // Простое сообщение
    SIMPLE_TEXT,
    // Сообщение с кнопкой
    INLINE_KEYBOARD_MARKUP
}

Остановимся немного на енуме StepCode и StepType. Когда мы выбираем ту или иную команду формируется сообщение, которое отправляется пользователю. Иногда нужно отправить несколько сообщений подряд. Например START и затем USER_INFO. botPause нужен в первую очередь, чтобы проинформировать пользователя о необходимости принятия решений. Некоторые сообщения приходят с кнопками. Для этого и нужен енум StepType


Непосредственная реализация приёма сообщений будет представлена в компоненте ApplicationListener


package ru.template.telegram.bot.kotlin.template.listener

import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Lazy
import org.springframework.context.event.EventListener
import org.springframework.stereotype.Component
import ru.template.telegram.bot.kotlin.template.enums.ExecuteStatus
import ru.template.telegram.bot.kotlin.template.event.TelegramReceivedCallbackEvent
import ru.template.telegram.bot.kotlin.template.event.TelegramReceivedMessageEvent
import ru.template.telegram.bot.kotlin.template.event.TelegramStepMessageEvent
import ru.template.telegram.bot.kotlin.template.repository.UsersRepository
import ru.template.telegram.bot.kotlin.template.service.MessageService
import ru.template.telegram.bot.kotlin.template.strategy.LogicContext
import ru.template.telegram.bot.kotlin.template.strategy.NextStepContext

@Component
class ApplicationListener(
    private val logicContext: LogicContext, // Основная бизнес логика
    private val nextStepContext: NextStepContext, // Выбор следующего этапа
    private val usersRepository: UsersRepository, // Слой СУБД
    private val messageService: MessageService // Сервис, который формирует объект для отрпавки сообщения в бота
) {

    // Слушаем событие TelegramReceivedMessageEvent
    inner class Message {
        @EventListener
        fun onApplicationEvent(event: TelegramReceivedMessageEvent) {
            logicContext.execute(chatId = event.chatId, message = event.message)
            val nextStepCode = nextStepContext.next(event.chatId, event.stepCode)
            if (nextStepCode != null) {
                stepMessageBean().onApplicationEvent(
                    TelegramStepMessageEvent(
                        chatId = event.chatId,
                        stepCode = nextStepCode
                    )
                )
            }
        }
    }

    // Слушаем событие TelegramStepMessageEvent
    inner class StepMessage {
        @EventListener
        fun onApplicationEvent(event: TelegramStepMessageEvent) {
            // Обновляем шаг
            usersRepository.updateUserStep(event.chatId, event.stepCode)
            // Отправляем сообщение в бота (и формируем)
            messageService.sendMessageToBot(event.chatId, event.stepCode)
        }
    }

    // Слшуаем событие TelegramReceivedCallbackEvent
    inner class CallbackMessage {
        @EventListener
        fun onApplicationEvent(event: TelegramReceivedCallbackEvent) {
            val nextStepCode = when (logicContext.execute(event.chatId, event.callback)) {
                ExecuteStatus.FINAL -> { // Если бизнес процесс одобрил переход на новый этап
                    nextStepContext.next(event.chatId, event.stepCode)
                }
                ExecuteStatus.NOTHING -> throw IllegalStateException("Не поддерживается")
            }
            if (nextStepCode != null) {
                // редирект на событие TelegramStepMessageEvent
                stepMessageBean().onApplicationEvent(
                    TelegramStepMessageEvent(
                        chatId = event.chatId,
                        stepCode = nextStepCode
                    )
                )
            }
        }
    }

    @Bean
    @Lazy
    // Бин поступления сообщения от пользователя
    fun messageBean(): Message = Message()

    @Bean
    @Lazy
    // Бин отправки сообщения ботом
    fun stepMessageBean(): StepMessage = StepMessage()

    @Bean
    @Lazy
    // Бин, который срабатывает в момент клика по кнопке
    fun callbackMessageBean(): CallbackMessage = CallbackMessage()

}

MessageService — сервис, который формирует объект Телеграм АПИ сообщения и делает запрос на отправку в бота


package ru.template.telegram.bot.kotlin.template.service

import org.springframework.context.ApplicationEventPublisher
import org.springframework.stereotype.Service
import org.telegram.telegrambots.meta.api.methods.BotApiMethod
import org.telegram.telegrambots.meta.api.methods.ParseMode
import org.telegram.telegrambots.meta.api.methods.send.SendMessage
import org.telegram.telegrambots.meta.api.objects.Message
import org.telegram.telegrambots.meta.api.objects.replykeyboard.InlineKeyboardMarkup
import org.telegram.telegrambots.meta.api.objects.replykeyboard.ReplyKeyboardRemove
import org.telegram.telegrambots.meta.api.objects.replykeyboard.buttons.InlineKeyboardButton
import ru.template.telegram.bot.kotlin.template.api.TelegramSender
import ru.template.telegram.bot.kotlin.template.dto.MarkupDataDto
import ru.template.telegram.bot.kotlin.template.dto.markup.DataModel
import ru.template.telegram.bot.kotlin.template.enums.StepCode
import ru.template.telegram.bot.kotlin.template.enums.StepType.*
import ru.template.telegram.bot.kotlin.template.event.TelegramStepMessageEvent
import ru.template.telegram.bot.kotlin.template.strategy.MarkupContext
import ru.template.telegram.bot.kotlin.template.strategy.MessageContext
import ru.template.telegram.bot.kotlin.template.strategy.NextStepContext

@Service
class MessageService(
    private val telegramSender: TelegramSender, // отправщик сообщения
    private val messageContext: MessageContext, // Формирование текстовок сообщения
    private val applicationEventPublisher: ApplicationEventPublisher,
    private val markupContext: MarkupContext<DataModel>, // Формирование текстовок с кнопками
    private val nextStepContext: NextStepContext // Выбор следующего этапа
) {

    fun sendMessageToBot(
        chatId: Long,
        stepCode: StepCode
    ) {
        when (stepCode.type) {
            // Простое сообщение
            SIMPLE_TEXT -> telegramSender.execute(simpleTextMessage(chatId))
            // Сообщение с кнопками
            INLINE_KEYBOARD_MARKUP -> telegramSender.sendInlineKeyboardMarkup(chatId)
        }

        if (!stepCode.botPause) { // если нет паузы, то формируем следующее сообщение
            applicationEventPublisher.publishEvent(
                TelegramStepMessageEvent(
                    chatId = chatId,
                    stepCode = nextStepContext.next(chatId, stepCode)!!
                )
            )
        }
    }

    // SendMessage - объект телеграм АПИ для отправки сообщения
    private fun simpleTextMessage(chatId: Long): SendMessage {
        val sendMessage = SendMessage()
        sendMessage.chatId = chatId.toString()
        sendMessage.text = messageContext.getMessage(chatId)
        sendMessage.enableHtml(true)

        return sendMessage
    }

    // Отправляем в бота сообщение с кнопками
    private fun TelegramSender.sendInlineKeyboardMarkup(chatId: Long) {
        val inlineKeyboardMarkup: InlineKeyboardMarkup
        val messageText: String

        val inlineKeyboardMarkupDto = markupContext.getInlineKeyboardMarkupDto(chatId)!!
        messageText = inlineKeyboardMarkupDto.message
        inlineKeyboardMarkup = inlineKeyboardMarkupDto.inlineButtons.getInlineKeyboardMarkup()

        this.execute(sendMessageWithMarkup(chatId, messageText, inlineKeyboardMarkup))
    }

    private fun sendMessageWithMarkup(
        chatId: Long, messageText: String, inlineKeyboardMarkup: InlineKeyboardMarkup
    ): BotApiMethod<Message> {
        val sendMessage = SendMessage()
        sendMessage.chatId = chatId.toString()
        sendMessage.text = messageText

        sendMessage.replyMarkup = inlineKeyboardMarkup
        sendMessage.parseMode = ParseMode.HTML
        return sendMessage
    }

    // Формируем модель кнопок
    private fun List<MarkupDataDto>.getInlineKeyboardMarkup(): InlineKeyboardMarkup {
        val inlineKeyboardMarkup = InlineKeyboardMarkup()
        val inlineKeyboardButtonsInner: MutableList<InlineKeyboardButton> = mutableListOf()
        val inlineKeyboardButtons: MutableList<MutableList<InlineKeyboardButton>> = mutableListOf()
        this.sortedBy { it.rowPos }.forEach { markupDataDto ->
            val button = InlineKeyboardButton()
                .also { it.text = markupDataDto.text }
                .also { it.callbackData = markupDataDto.text }
            inlineKeyboardButtonsInner.add(button)
        }
        inlineKeyboardButtons.add(inlineKeyboardButtonsInner)
        inlineKeyboardMarkup.keyboard = inlineKeyboardButtons
        return inlineKeyboardMarkup
    }
}

Сам отправщик сообщений выглядит проще. Все механизмы в библиотеке, но для ее реализации нужно создать класс


package ru.template.telegram.bot.kotlin.template.api

import javax.annotation.PostConstruct
import org.springframework.stereotype.Component
import org.telegram.telegrambots.extensions.bots.commandbot.TelegramLongPollingCommandBot
import org.telegram.telegrambots.extensions.bots.commandbot.commands.IBotCommand
import org.telegram.telegrambots.meta.api.methods.commands.SetMyCommands
import org.telegram.telegrambots.meta.api.methods.send.SendMessage
import org.telegram.telegrambots.meta.api.objects.Update
import org.telegram.telegrambots.meta.api.objects.commands.BotCommand
import org.telegram.telegrambots.meta.api.objects.commands.scope.BotCommandScopeChat
import ru.template.telegram.bot.kotlin.template.properties.BotProperty
import ru.template.telegram.bot.kotlin.template.service.ReceiverService

@Component
class TelegramSender(
    private val botProperty: BotProperty,
    private val botCommands: List<IBotCommand>,
    private val receiverService: ReceiverService
) : TelegramLongPollingCommandBot() {

    @PostConstruct
    // Регистрация команд в системе
    fun initCommands() {
        botCommands.forEach {
            register(it)
        }

        registerDefaultAction { absSender, message ->

            val commandUnknownMessage = SendMessage()
            commandUnknownMessage.chatId = message.chatId.toString()
            commandUnknownMessage.text = "Command '" + message.text.toString() + "' unknown"

            absSender.execute(commandUnknownMessage)
        }
    }

    // токен. Формируем в @BotFather
    override fun getBotToken() = botProperty.token

    // username. Формируем в @BotFather
    override fun getBotUsername() = botProperty.username

    // событие, которое пришли от пользователя (кромер команд)
    override fun processNonCommandUpdate(update: Update) {
        receiverService.execute(update)
    }
}

TelegramLongPollingCommandBot — это базовый класс Телеграм АПИ, который отправляет и принимает сообщения. Хочу отметить, что в примере есть проперти, который нужно задать через BotFather


Осталось дело за малым. Сервис приёма сообщений ReceiverService непосредственно принимает текст, введенный пользователем или мета информацию по кнопке.


package ru.template.telegram.bot.kotlin.template.service

import org.springframework.context.ApplicationEventPublisher
import org.springframework.stereotype.Service
import org.telegram.telegrambots.meta.api.objects.CallbackQuery
import org.telegram.telegrambots.meta.api.objects.Message
import org.telegram.telegrambots.meta.api.objects.Update
import ru.template.telegram.bot.kotlin.template.enums.StepCode
import ru.template.telegram.bot.kotlin.template.event.TelegramReceivedCallbackEvent
import ru.template.telegram.bot.kotlin.template.event.TelegramReceivedMessageEvent
import ru.template.telegram.bot.kotlin.template.repository.UsersRepository

@Service
class ReceiverService(
    private val applicationEventPublisher: ApplicationEventPublisher,
    private val usersRepository: UsersRepository
) {

    // выходной метод сервиса
    fun execute(update: Update) {
        if (update.hasCallbackQuery()) { // Выполнить, если это действие по кнопке
            callbackExecute(update.callbackQuery)
        } else if (update.hasMessage()) { // Выполнить, если это сообщение пользователя
            messageExecute(update.message)
        } else {
            throw IllegalStateException("Not yet supported")
        }
    }

    private fun messageExecute(message: Message) {
        val chatId = message.chatId
        val stepCode = usersRepository.getUser(chatId)!!.stepCode // Выбор текущего шага
        applicationEventPublisher.publishEvent( // Формируем событие TelegramReceivedMessageEvent
            TelegramReceivedMessageEvent(
                chatId = chatId,
                stepCode = StepCode.valueOf(stepCode),
                message = message
            )
        )
    }

    private fun callbackExecute(callback: CallbackQuery) {
        val chatId = callback.from.id
        val stepCode = usersRepository.getUser(chatId)!!.stepCode // Выбор текущего шага
        applicationEventPublisher.publishEvent( // Формируем событие TelegramReceivedCallbackEvent
            TelegramReceivedCallbackEvent(chatId = chatId, stepCode = StepCode.valueOf(stepCode), callback = callback)
        )
    }
}

Здесь всё просто, если Кнопка, то событие для кнопки, если текст, то событие для обработки текста.


Бизнес процессы


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


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



Заключение


Итого, что мы имеем? Описанный код продемонстирировал "поляну" для того, чтобы разработчики накидали классы, в которых будет реализована основная бизнес логика (добавление данных в базу, действие по кнопке). Также можно реализовать бизнес логику на роутинг следующего шага. Конечно, данная статья не поможет реалзизовать достаточно сложного бота. Однако, поработав с аналитиком и архитектором, можно с лёгкостью реализовать новые слои в пакете strategy.

Источник: https://habr.com/ru/post/588474/


Интересные статьи

Интересные статьи

Однажды я заметил, что один из моих скриптов, сканирующих почтовые журналы, не выдал сообщение об одной записи, о наличии которой в журнале мне было известно (о ней меня оповестил дру...
Так получилось, что наша с мужем ИТ-эмиграция пришлась на непростой период - только мы начали привыкать к новой жизни в центре Европы, как наступила весна 2020 года со вс...
Большинство списков вопросов интервью по Spring Boot заставляют вас запоминать случайные детали из документации Spring Boot. Но запоминание — плохая замена истинному пони...
Предлагаю окунуться в дебри микроархитектуры компьютера и разобраться с тем, как работает одна из наиболее распространенных технологий обеспечения аппаратной целостности ...
Привет, Хабр! Представляю вашему вниманию перевод статьи "How does a relational database work". Когда дело доходит до реляционных баз данных я не могу не думать, что чего-то не хватае...