recurrents

This commit is contained in:
xds
2025-11-17 15:02:47 +03:00
parent d0cae182b7
commit 12afd1f90e
48 changed files with 1479 additions and 120 deletions

View File

@@ -0,0 +1,253 @@
package space.luminic.finance.services.telegram
import com.github.kotlintelegrambot.Bot
import com.github.kotlintelegrambot.dispatch
import com.github.kotlintelegrambot.dispatcher.callbackQuery
import com.github.kotlintelegrambot.dispatcher.command
import com.github.kotlintelegrambot.dispatcher.message
import com.github.kotlintelegrambot.entities.ChatId
import com.github.kotlintelegrambot.entities.InlineKeyboardMarkup
import com.github.kotlintelegrambot.entities.ParseMode
import com.github.kotlintelegrambot.entities.keyboard.InlineKeyboardButton
import com.github.kotlintelegrambot.entities.keyboard.WebAppInfo
import com.github.kotlintelegrambot.entities.reaction.ReactionType
import com.github.kotlintelegrambot.extensions.filters.Filter
import com.github.kotlintelegrambot.logging.LogLevel
import org.springframework.beans.factory.annotation.Qualifier
import org.springframework.beans.factory.annotation.Value
import org.springframework.context.annotation.Bean
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import space.luminic.finance.dtos.TransactionDTO
import space.luminic.finance.models.NotFoundException
import space.luminic.finance.models.State
import space.luminic.finance.models.Transaction
import space.luminic.finance.models.User
import space.luminic.finance.repos.BotRepo
import space.luminic.finance.services.UserService
import java.time.LocalDate
@Service
class BotService(
@Value("\${telegram.bot.token}") private val botToken: String,
private val userService: UserService,
@Qualifier("spaceServiceTelegram") private val spaceService: SpaceService,
private val botRepo: BotRepo,
@Qualifier("transactionsServiceTelegram") private val transactionService: TransactionService
) {
private fun buildSpaceSelector(userId: Int): InlineKeyboardMarkup {
val spaces = spaceService.getSpaces(userId)
val keyboard = mutableListOf<List<InlineKeyboardButton>>()
val row = mutableListOf<InlineKeyboardButton>()
if (spaces.isNotEmpty()) {
for ((index, space) in spaces.withIndex()) {
val button =
InlineKeyboardButton.CallbackData(text = space.name, callbackData = "select_space_${space.id}")
row.add(button)
// Если 2 кнопки в строке — отправляем строку и очищаем
if (row.size == 2) {
keyboard.add(ArrayList(row))
row.clear()
}
}
// Если осталась 1 кнопка — добавляем последнюю строку
if (row.isNotEmpty()) {
keyboard.add(ArrayList(row))
}
} else {
row.add(InlineKeyboardButton.CallbackData("Создать пространство!", callbackData = "create_space"))
keyboard.add(ArrayList(row))
}
return InlineKeyboardMarkup.Companion.create(keyboard)
}
@Transactional
fun selectSpace(tgUserId: Long, selectedSpaceId: Int) {
val user = userService.getUserByTelegramId(tgUserId)
botRepo.setState(
user.id!!, State.StateCode.SPACE_SELECTED, mapOf(
"selected_space" to selectedSpaceId.toString(),
)
)
}
private fun buildRegister() {
}
private fun buildMenu(tgUserId: Long): InlineKeyboardMarkup {
val user = userService.getUserByTelegramId(tgUserId)
val userId = requireNotNull(user.id) { "User must have id" }
val state = botRepo.getState(tgUserId)
val spaceId = state?.data?.get("selected_space")?.toIntOrNull()
val space = spaceId?.let { id -> spaceService.getSpace(spaceId, userId) }
val keyboard = mutableListOf<List<InlineKeyboardButton>>()
// Кнопка с названием выбранного space (или плейсхолдером)
keyboard.add(
listOf(
InlineKeyboardButton.CallbackData(
text = space?.name ?: "Select space",
"select_space"
)
)
)
// Если нужен второй баттон — сделай другой смысл/текст; иначе этот блок можно убрать
keyboard.add(
listOf(
InlineKeyboardButton.WebApp(
text = "Открыть WebApp",
webApp = WebAppInfo(url = "https://app.luminic.space")
)
)
)
return InlineKeyboardMarkup.Companion.create(keyboard)
}
@Bean
fun bot(): Bot {
val bot = com.github.kotlintelegrambot.bot {
logLevel = LogLevel.None
token = botToken
dispatch {
message(Filter.Text) {
val fromId = message.from?.id ?: throw IllegalArgumentException("user is empty")
val user = userService.getUserByTelegramId(fromId)
val state = botRepo.getState(message.from?.id ?: throw IllegalArgumentException("user is empty"))
when (state?.state) {
State.StateCode.SPACE_SELECTED -> {
try {
val parts = message.text!!.trim().split(" ", limit = 2)
if (parts.isEmpty()) {
bot.sendMessage(
chatId = ChatId.fromId(message.chat.id),
text = "Введите сумму и комментарий, например: `250 обед`",
parseMode = ParseMode.MARKDOWN
)
}
val amount = parts[0].toIntOrNull()
?: throw IllegalArgumentException("Сумма транзакции не число!")
if (amount <= 0) {
throw IllegalArgumentException("Сумма не может быть меньше 1.")
}
val comment = parts.getOrNull(1)?.trim().orEmpty()
if (comment.isEmpty()) throw IllegalArgumentException("Комментарий не может быть пустым.")
// bot.sendMessage(
// chatId = ChatId.fromId(message.chat.id),
// text = "Принято: сумма = $amount, комментарий = \"$comment\""
// )
try {
transactionService.createTransaction(
state.data["selected_space"]?.toInt()
?: throw IllegalArgumentException("selected space is empty"),
user.id!!,
TransactionDTO.CreateTransactionDTO(
Transaction.TransactionType.EXPENSE,
Transaction.TransactionKind.INSTANT,
comment = comment,
amount = amount.toBigDecimal(),
date = LocalDate.now(),
),
message.chat.id,
message.messageId
)
bot.setMessageReaction(
chatId = ChatId.fromId(message.chat.id),
messageId = message.messageId,
reaction = listOf(ReactionType.Emoji("🤝")),
isBig = false
)
} catch (e: IllegalArgumentException) {
bot.sendMessage(
ChatId.Companion.fromId(message.chat.id),
text = "Кажется у вас не выбран Space",
replyMarkup = buildSpaceSelector(user.id!!)
)
}
} catch (e: IllegalArgumentException) {
bot.sendMessage(
chatId = ChatId.Companion.fromId(message.chat.id),
text = "Ошибка: ${e.message}"
)
}
}
else -> {}
}
}
callbackQuery {
if (callbackQuery.data.startsWith("select_space_")) {
val spaceId = callbackQuery.data.substringAfter("select_space_").toInt()
println(spaceId)
try {
selectSpace(callbackQuery.from.id, spaceId)
bot.editMessageText(
chatId = ChatId.Companion.fromId(callbackQuery.message!!.chat.id),
messageId = callbackQuery.message!!.messageId,
text = "Успешно!\n\nМы готовы принимать Ваши транзакции.\n\nПросто пишите их в формате:\n\n <i>сумма комментарий</i>\n\n <b>Первой обязательно должна быть сумма!</b>",
parseMode = ParseMode.HTML,
replyMarkup = buildMenu(callbackQuery.from.id)
)
} catch (e: NotFoundException) {
e.printStackTrace()
bot.sendMessage(
ChatId.Companion.fromId(callbackQuery.message!!.chat.id),
text = "Мы кажется не знакомы"
)
}
} else if (callbackQuery.data.equals("select_space", ignoreCase = true)) {
bot.editMessageText(
ChatId.Companion.fromId(callbackQuery.message!!.chat.id),
callbackQuery.message!!.messageId,
text = "Выберите новое пространство",
replyMarkup = buildSpaceSelector(
userService.getUserByTelegramId(callbackQuery.from.id).id!!
)
)
}
}
command("start") {
val user: User
try {
user = userService.getUserByTelegramId(
message.from?.id ?: throw IllegalArgumentException("User not found")
)
bot.sendMessage(
ChatId.Companion.fromId(message.chat.id),
text = "Привет!\n\nРады тебя снова видеть!\n\nНачнем с выбора пространства:",
replyMarkup = buildSpaceSelector(user.id!!)
)
} catch (e: NotFoundException) {
bot.sendMessage(ChatId.Companion.fromId(message.chat.id), text = "Кажется, мы еще не знакомы.")
bot.sendMessage(ChatId.Companion.fromId(message.chat.id), text = "Давайте зарегистрируемся? ")
bot.sendMessage(ChatId.Companion.fromId(message.chat.id), text = "")
}
}
}
}
bot.startPolling()
return bot
}
}

View File

@@ -0,0 +1,8 @@
package space.luminic.finance.services.telegram
import space.luminic.finance.models.Space
interface SpaceService {
fun getSpaces(userId: Int): List<Space>
fun getSpace(spaceId: Int, userId: Int): Space?
}

View File

@@ -0,0 +1,23 @@
package space.luminic.finance.services.telegram
import org.springframework.stereotype.Service
import space.luminic.finance.models.NotFoundException
import space.luminic.finance.models.Space
import space.luminic.finance.repos.SpaceRepo
@Service("spaceServiceTelegram")
class SpaceServiceImpl(
private val spaceRepo: SpaceRepo
) : SpaceService {
override fun getSpaces(userId: Int): List<Space> {
val spaces = spaceRepo.findSpacesAvailableForUser(userId)
return spaces
}
override fun getSpace(spaceId: Int, userId: Int): Space? {
val space =
spaceRepo.findSpaceById(spaceId, userId) ?: throw NotFoundException("Space with id $spaceId not found")
return space
}
}

View File

@@ -0,0 +1,8 @@
package space.luminic.finance.services.telegram
import space.luminic.finance.dtos.TransactionDTO
interface TransactionService {
fun createTransaction(spaceId: Int, userId: Int, transaction: TransactionDTO.CreateTransactionDTO, chatId: Long, messageId: Long ): Int
}

View File

@@ -0,0 +1,44 @@
package space.luminic.finance.services.telegram
import org.springframework.beans.factory.annotation.Qualifier
import org.springframework.stereotype.Service
import space.luminic.finance.dtos.TransactionDTO
import space.luminic.finance.models.Transaction
import space.luminic.finance.repos.TransactionRepo
import space.luminic.finance.services.CategoryServiceImpl
@Service("transactionsServiceTelegram")
class TransactionsServiceImpl(
private val transactionRepo: TransactionRepo,
@Qualifier("spaceServiceTelegram") private val spaceService: SpaceService,
private val categoryService: CategoryServiceImpl
): TransactionService {
override fun createTransaction(
spaceId: Int,
userId: Int,
transaction: TransactionDTO.CreateTransactionDTO,
chatId: Long,
messageId: Long
): Int {
val space = spaceService.getSpace(spaceId, userId)
val category = transaction.categoryId?.let { categoryService.getCategory(spaceId, it) }
val transaction = Transaction(
space = space,
type = transaction.type,
kind = transaction.kind,
category = category,
comment = transaction.comment,
amount = transaction.amount,
fees = transaction.fees,
date = transaction.date,
tgChatId = chatId,
tgMessageId = messageId,
)
print(transaction)
return transactionRepo.create(transaction, userId)
}
}