В этой статье я расскажу о том, как быстро и легко разработать свой собственный телеграм-бот на языке Kotlin с использованием Spring Boot.
Основная задумка заключается в том, чтобы архитектура Бота была абстрагирована от бизнес процессов. То есть акцент должен быть задействован именно на написании кода, который нужен только бизнесу.
Мой выбор пал на следующий стек технологий:
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 = "jdbcostgresql://localhost:5432/kotlin_template"
user = "postgres"
password = "postgres"
}
// список зависимстей
dependencies {
flywayMigration("org.postgresqlostgresql:$postgresVersion")
jooqGenerator("org.postgresqlostgresql:$postgresVersion")
runtimeOnly("org.postgresqlostgresql")
//Классические стартеры 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. Там будут лежать и прочие компоненты нашей архитектуры.
Создадим пакеты:
Нам необходимо создать такой компонент, который бы принимал наши сообщения от Бота и начинал их обрабатывать.
Создадим файл application.yml
# Настройка для телеграм апи
bot:
username: kotlin_template_bot
token: [your bot token here]
# Настройка для СУБД
spring:
datasource:
url: jdbcostgresql://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.
Основная задумка заключается в том, чтобы архитектура Бота была абстрагирована от бизнес процессов. То есть акцент должен быть задействован именно на написании кода, который нужен только бизнесу.
Технологии
Мой выбор пал на следующий стек технологий:
- 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 = "jdbcostgresql://localhost:5432/kotlin_template"
user = "postgres"
password = "postgres"
}
// список зависимстей
dependencies {
flywayMigration("org.postgresqlostgresql:$postgresVersion")
jooqGenerator("org.postgresqlostgresql:$postgresVersion")
runtimeOnly("org.postgresqlostgresql")
//Классические стартеры 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: jdbcostgresql://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.
Разработка телеграм-бота на Kotlin + Spring Boot
Привет, читателям Хабра! В этой статье я расскажу о том, как быстро и легко разработать свой собственный телеграм-бот на языке Kotlin с использованием Spring Boot. Основная задумка заключается в том,...
habr.com