Разработка телеграм-бота на 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 мире, начнем работу с описании зависимостей.

build.gradle.kts

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-файл. В интернете много примеров. Сейчас нам не так это важно.

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

Application.kt

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

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

BotProperty.kt

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 = "" ) 

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

CommandCode.kt

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

V1__init.sql

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

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

UsersRepository.kt

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

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

StartCommand.kt

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

TelegramStepMessageEvent.kt

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, который носит информацию о типе сообщения, о шаге и прочую системную информацию

StepCode.kt

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

ApplicationListener.kt

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 — сервис, который формирует объект Телеграм АПИ сообщения и делает запрос на отправку в бота

MessageService.kt

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     } }

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

TelegramSender.kt

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 непосредственно принимает текст, введенный пользователем или мета информацию по кнопке.

ReceiverService.kt

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/articles/588474/


Комментарии

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

Ваш адрес email не будет опубликован. Обязательные поля помечены *