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

Kate

Administrator
Команда форума
В этой статье я расскажу о том, как быстро и легко разработать свой собственный телеграм-бот на языке 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)
)
}
}

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


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


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


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


ymd5fky8pgrz7jeleggb4z6gup8.png



Заключение​


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

 
Сверху