+ bot + notifications

This commit is contained in:
xds
2025-04-01 15:07:50 +03:00
parent 711348b386
commit a38d5068e0
22 changed files with 603 additions and 129 deletions

View File

@@ -56,6 +56,8 @@ dependencies {
implementation("com.google.code.gson:gson") implementation("com.google.code.gson:gson")
implementation("io.micrometer:micrometer-registry-prometheus") implementation("io.micrometer:micrometer-registry-prometheus")
implementation("org.telegram:telegrambots:6.9.7.1")
implementation("org.telegram:telegrambots-spring-boot-starter:6.9.7.1")

View File

@@ -2,17 +2,20 @@ package space.luminic.budgerapp
import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.boot.runApplication import org.springframework.boot.runApplication
import org.springframework.cache.annotation.EnableCaching import org.springframework.cache.annotation.EnableCaching
import org.springframework.data.mongodb.repository.config.EnableMongoRepositories import org.springframework.data.mongodb.repository.config.EnableMongoRepositories
import org.springframework.scheduling.annotation.EnableAsync import org.springframework.scheduling.annotation.EnableAsync
import org.springframework.scheduling.annotation.EnableScheduling import org.springframework.scheduling.annotation.EnableScheduling
import java.util.TimeZone import space.luminic.budgerapp.configs.TelegramBotProperties
import java.util.*
@SpringBootApplication(scanBasePackages = ["space.luminic.budgerapp"]) @SpringBootApplication(scanBasePackages = ["space.luminic.budgerapp"])
@EnableCaching @EnableCaching
@EnableAsync @EnableAsync
@EnableScheduling @EnableScheduling
@EnableConfigurationProperties(TelegramBotProperties::class)
@EnableMongoRepositories(basePackages = ["space.luminic.budgerapp.repos"]) @EnableMongoRepositories(basePackages = ["space.luminic.budgerapp.repos"])
class BudgerAppApplication class BudgerAppApplication

View File

@@ -0,0 +1,25 @@
package space.luminic.budgerapp.configs
import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.telegram.telegrambots.meta.TelegramBotsApi
import org.telegram.telegrambots.updatesreceivers.DefaultBotSession
import space.luminic.budgerapp.services.BotService
@Configuration
class TelegramBotConfig {
@Bean
fun telegramBotsApi(myBot: BotService): TelegramBotsApi {
val botsApi = TelegramBotsApi(DefaultBotSession::class.java)
botsApi.registerBot(myBot)
return botsApi
}
}
@ConfigurationProperties(prefix = "telegram.bot")
data class TelegramBotProperties(
val username: String,
val token: String
)

View File

@@ -10,10 +10,7 @@ import org.springframework.web.bind.annotation.*
import space.luminic.budgerapp.controllers.BudgetController.LimitValue import space.luminic.budgerapp.controllers.BudgetController.LimitValue
import space.luminic.budgerapp.controllers.dtos.BudgetCreationDTO import space.luminic.budgerapp.controllers.dtos.BudgetCreationDTO
import space.luminic.budgerapp.models.* import space.luminic.budgerapp.models.*
import space.luminic.budgerapp.services.CategoryService import space.luminic.budgerapp.services.*
import space.luminic.budgerapp.services.FinancialService
import space.luminic.budgerapp.services.RecurrentService
import space.luminic.budgerapp.services.SpaceService
import java.time.LocalDate import java.time.LocalDate
@RestController @RestController
@@ -22,7 +19,8 @@ class SpaceController(
private val spaceService: SpaceService, private val spaceService: SpaceService,
private val financialService: FinancialService, private val financialService: FinancialService,
private val categoryService: CategoryService, private val categoryService: CategoryService,
private val recurrentService: RecurrentService private val recurrentService: RecurrentService,
private val authService: AuthService
) { ) {
private val log = LoggerFactory.getLogger(SpaceController::class.java) private val log = LoggerFactory.getLogger(SpaceController::class.java)
@@ -57,13 +55,16 @@ class SpaceController(
@DeleteMapping("/{spaceId}") @DeleteMapping("/{spaceId}")
suspend fun deleteSpace(@PathVariable spaceId: String) { suspend fun deleteSpace(@PathVariable spaceId: String) {
return spaceService.deleteSpace(spaceService.isValidRequest(spaceId)) val user = authService.getSecurityUser()
val space = spaceService.isValidRequest(spaceId, user)
return spaceService.deleteSpace(space)
} }
@PostMapping("/{spaceId}/invite") @PostMapping("/{spaceId}/invite")
suspend fun inviteSpace(@PathVariable spaceId: String): SpaceInvite { suspend fun inviteSpace(@PathVariable spaceId: String): SpaceInvite {
spaceService.isValidRequest(spaceId) val user = authService.getSecurityUser()
spaceService.isValidRequest(spaceId, user)
return spaceService.createInviteSpace(spaceId) return spaceService.createInviteSpace(spaceId)
} }
@@ -74,13 +75,15 @@ class SpaceController(
@DeleteMapping("/{spaceId}/leave") @DeleteMapping("/{spaceId}/leave")
suspend fun leaveSpace(@PathVariable spaceId: String) { suspend fun leaveSpace(@PathVariable spaceId: String) {
spaceService.isValidRequest(spaceId) val user = authService.getSecurityUser()
spaceService.isValidRequest(spaceId, user)
return spaceService.leaveSpace(spaceId) return spaceService.leaveSpace(spaceId)
} }
@DeleteMapping("/{spaceId}/members/kick/{username}") @DeleteMapping("/{spaceId}/members/kick/{username}")
suspend fun kickMembers(@PathVariable spaceId: String, @PathVariable username: String) { suspend fun kickMembers(@PathVariable spaceId: String, @PathVariable username: String) {
spaceService.isValidRequest(spaceId) val user = authService.getSecurityUser()
spaceService.isValidRequest(spaceId, user)
return spaceService.kickMember(spaceId, username) return spaceService.kickMember(spaceId, username)
} }
@@ -90,7 +93,8 @@ class SpaceController(
// //
@GetMapping("/{spaceId}/budgets") @GetMapping("/{spaceId}/budgets")
suspend fun getBudgets(@PathVariable spaceId: String): List<Budget> { suspend fun getBudgets(@PathVariable spaceId: String): List<Budget> {
spaceService.isValidRequest(spaceId) val user = authService.getSecurityUser()
spaceService.isValidRequest(spaceId, user)
return financialService.getBudgets(spaceId).awaitSingleOrNull().orEmpty() return financialService.getBudgets(spaceId).awaitSingleOrNull().orEmpty()
} }
@@ -98,7 +102,8 @@ class SpaceController(
@GetMapping("/{spaceId}/budgets/{id}") @GetMapping("/{spaceId}/budgets/{id}")
suspend fun getBudget(@PathVariable spaceId: String, @PathVariable id: String): BudgetDTO? { suspend fun getBudget(@PathVariable spaceId: String, @PathVariable id: String): BudgetDTO? {
log.info("Getting budget for spaceId=$spaceId, id=$id") log.info("Getting budget for spaceId=$spaceId, id=$id")
spaceService.isValidRequest(spaceId) val user = authService.getSecurityUser()
spaceService.isValidRequest(spaceId, user)
return financialService.getBudget(spaceId, id) return financialService.getBudget(spaceId, id)
} }
@@ -107,8 +112,10 @@ class SpaceController(
@PathVariable spaceId: String, @PathVariable spaceId: String,
@RequestBody budgetCreationDTO: BudgetCreationDTO, @RequestBody budgetCreationDTO: BudgetCreationDTO,
): Budget? { ): Budget? {
val user = authService.getSecurityUser()
val space = spaceService.isValidRequest(spaceId, user)
return financialService.createBudget( return financialService.createBudget(
spaceService.isValidRequest(spaceId), space,
budgetCreationDTO.budget, budgetCreationDTO.budget,
budgetCreationDTO.createRecurrent budgetCreationDTO.createRecurrent
) )
@@ -116,7 +123,8 @@ class SpaceController(
@DeleteMapping("/{spaceId}/budgets/{id}") @DeleteMapping("/{spaceId}/budgets/{id}")
suspend fun deleteBudget(@PathVariable spaceId: String, @PathVariable id: String) { suspend fun deleteBudget(@PathVariable spaceId: String, @PathVariable id: String) {
spaceService.isValidRequest(spaceId) val user = authService.getSecurityUser()
spaceService.isValidRequest(spaceId, user)
financialService.deleteBudget(spaceId, id) financialService.deleteBudget(spaceId, id)
} }
@@ -128,7 +136,8 @@ class SpaceController(
@PathVariable catId: String, @PathVariable catId: String,
@RequestBody limit: LimitValue, @RequestBody limit: LimitValue,
): BudgetCategory { ): BudgetCategory {
spaceService.isValidRequest(spaceId) val user = authService.getSecurityUser()
spaceService.isValidRequest(spaceId, user)
return financialService.setCategoryLimit(spaceId, budgetId, catId, limit.limit) return financialService.setCategoryLimit(spaceId, budgetId, catId, limit.limit)
} }
@@ -174,7 +183,8 @@ class SpaceController(
@PostMapping("/{spaceId}/transactions") @PostMapping("/{spaceId}/transactions")
suspend fun createTransaction(@PathVariable spaceId: String, @RequestBody transaction: Transaction): Transaction { suspend fun createTransaction(@PathVariable spaceId: String, @RequestBody transaction: Transaction): Transaction {
val space = spaceService.isValidRequest(spaceId) val user = authService.getSecurityUser()
val space = spaceService.isValidRequest(spaceId, user)
return financialService.createTransaction(space, transaction) return financialService.createTransaction(space, transaction)
@@ -184,14 +194,16 @@ class SpaceController(
suspend fun editTransaction( suspend fun editTransaction(
@PathVariable spaceId: String, @PathVariable id: String, @RequestBody transaction: Transaction @PathVariable spaceId: String, @PathVariable id: String, @RequestBody transaction: Transaction
): Transaction { ): Transaction {
val space = spaceService.isValidRequest(spaceId) val user = authService.getSecurityUser()
val space = spaceService.isValidRequest(spaceId, user)
transaction.space = space transaction.space = space
return financialService.editTransaction(transaction) return financialService.editTransaction(transaction, user)
} }
@DeleteMapping("/{spaceId}/transactions/{id}") @DeleteMapping("/{spaceId}/transactions/{id}")
suspend fun deleteTransaction(@PathVariable spaceId: String, @PathVariable id: String) { suspend fun deleteTransaction(@PathVariable spaceId: String, @PathVariable id: String) {
spaceService.isValidRequest(spaceId) val user = authService.getSecurityUser()
spaceService.isValidRequest(spaceId, user)
val transaction = financialService.getTransactionById(id) val transaction = financialService.getTransactionById(id)
financialService.deleteTransaction(transaction) financialService.deleteTransaction(transaction)
} }
@@ -206,7 +218,8 @@ class SpaceController(
@RequestParam("sort") sortBy: String = "name", @RequestParam("sort") sortBy: String = "name",
@RequestParam("direction") direction: String = "ASC" @RequestParam("direction") direction: String = "ASC"
): List<Category> { ): List<Category> {
spaceService.isValidRequest(spaceId) val user = authService.getSecurityUser()
spaceService.isValidRequest(spaceId, user)
return categoryService.getCategories(spaceId, type, sortBy, direction).awaitSingleOrNull().orEmpty() return categoryService.getCategories(spaceId, type, sortBy, direction).awaitSingleOrNull().orEmpty()
} }
@@ -219,7 +232,8 @@ class SpaceController(
suspend fun createCategory( suspend fun createCategory(
@PathVariable spaceId: String, @RequestBody category: Category @PathVariable spaceId: String, @RequestBody category: Category
): Category { ): Category {
val space = spaceService.isValidRequest(spaceId) val user = authService.getSecurityUser()
val space = spaceService.isValidRequest(spaceId, user)
return financialService.createCategory(space, category).awaitSingle() return financialService.createCategory(space, category).awaitSingle()
} }
@@ -229,33 +243,38 @@ class SpaceController(
@RequestBody category: Category, @RequestBody category: Category,
@PathVariable spaceId: String @PathVariable spaceId: String
): Category { ): Category {
val space = spaceService.isValidRequest(spaceId) val user = authService.getSecurityUser()
val space = spaceService.isValidRequest(spaceId, user)
return categoryService.editCategory(space, category) return categoryService.editCategory(space, category)
} }
@DeleteMapping("/{spaceId}/categories/{categoryId}") @DeleteMapping("/{spaceId}/categories/{categoryId}")
suspend fun deleteCategory(@PathVariable categoryId: String, @PathVariable spaceId: String) { suspend fun deleteCategory(@PathVariable categoryId: String, @PathVariable spaceId: String) {
val space = spaceService.isValidRequest(spaceId) val user = authService.getSecurityUser()
categoryService.deleteCategory(space, categoryId) val space = spaceService.isValidRequest(spaceId, user)
categoryService.deleteCategory(space, categoryId, user)
} }
@GetMapping("/{spaceId}/categories/tags") @GetMapping("/{spaceId}/categories/tags")
suspend fun getTags(@PathVariable spaceId: String): List<Tag> { suspend fun getTags(@PathVariable spaceId: String): List<Tag> {
val space = spaceService.isValidRequest(spaceId) val user = authService.getSecurityUser()
val space = spaceService.isValidRequest(spaceId, user)
return spaceService.getTags(space) return spaceService.getTags(space)
} }
@PostMapping("/{spaceId}/categories/tags") @PostMapping("/{spaceId}/categories/tags")
suspend fun createTags(@PathVariable spaceId: String, @RequestBody tag: Tag): Tag { suspend fun createTags(@PathVariable spaceId: String, @RequestBody tag: Tag): Tag {
val space = spaceService.isValidRequest(spaceId) val user = authService.getSecurityUser()
val space = spaceService.isValidRequest(spaceId, user)
return spaceService.createTag(space, tag) return spaceService.createTag(space, tag)
} }
@DeleteMapping("/{spaceId}/categories/tags/{tagId}") @DeleteMapping("/{spaceId}/categories/tags/{tagId}")
suspend fun deleteTags(@PathVariable spaceId: String, @PathVariable tagId: String) { suspend fun deleteTags(@PathVariable spaceId: String, @PathVariable tagId: String) {
val space = spaceService.isValidRequest(spaceId) val user = authService.getSecurityUser()
val space = spaceService.isValidRequest(spaceId, user)
return spaceService.deleteTag(space, tagId) return spaceService.deleteTag(space, tagId)
} }
@@ -271,7 +290,8 @@ class SpaceController(
@GetMapping("/{spaceId}/recurrents") @GetMapping("/{spaceId}/recurrents")
suspend fun getRecurrents(@PathVariable spaceId: String): List<Recurrent> { suspend fun getRecurrents(@PathVariable spaceId: String): List<Recurrent> {
spaceService.isValidRequest(spaceId) val user = authService.getSecurityUser()
spaceService.isValidRequest(spaceId, user)
return recurrentService.getRecurrents(spaceId) return recurrentService.getRecurrents(spaceId)
} }
@@ -279,13 +299,15 @@ class SpaceController(
@GetMapping("/{spaceId}/recurrents/{id}") @GetMapping("/{spaceId}/recurrents/{id}")
suspend fun getRecurrent(@PathVariable spaceId: String, @PathVariable id: String): Recurrent { suspend fun getRecurrent(@PathVariable spaceId: String, @PathVariable id: String): Recurrent {
val space = spaceService.isValidRequest(spaceId) val user = authService.getSecurityUser()
val space = spaceService.isValidRequest(spaceId, user)
return recurrentService.getRecurrentById(space, id).awaitSingle() return recurrentService.getRecurrentById(space, id).awaitSingle()
} }
@PostMapping("/{spaceId}/recurrent") @PostMapping("/{spaceId}/recurrent")
suspend fun createRecurrent(@PathVariable spaceId: String, @RequestBody recurrent: Recurrent): Recurrent { suspend fun createRecurrent(@PathVariable spaceId: String, @RequestBody recurrent: Recurrent): Recurrent {
val space = spaceService.isValidRequest(spaceId) val user = authService.getSecurityUser()
val space = spaceService.isValidRequest(spaceId, user)
return recurrentService.createRecurrent(space, recurrent).awaitSingle() return recurrentService.createRecurrent(space, recurrent).awaitSingle()
} }
@@ -295,14 +317,16 @@ class SpaceController(
@PathVariable id: String, @PathVariable id: String,
@RequestBody recurrent: Recurrent @RequestBody recurrent: Recurrent
): Recurrent { ): Recurrent {
spaceService.isValidRequest(spaceId) val user = authService.getSecurityUser()
spaceService.isValidRequest(spaceId, user)
return recurrentService.editRecurrent(recurrent).awaitSingle() return recurrentService.editRecurrent(recurrent).awaitSingle()
} }
@DeleteMapping("/{spaceId}/recurrent/{id}") @DeleteMapping("/{spaceId}/recurrent/{id}")
suspend fun deleteRecurrent(@PathVariable spaceId: String, @PathVariable id: String) { suspend fun deleteRecurrent(@PathVariable spaceId: String, @PathVariable id: String) {
spaceService.isValidRequest(spaceId) val user = authService.getSecurityUser()
spaceService.isValidRequest(spaceId, user)
recurrentService.deleteRecurrent(id).awaitSingle() recurrentService.deleteRecurrent(id).awaitSingle()
} }

View File

@@ -35,7 +35,7 @@ class SubscriptionController(
} }
@PostMapping("/notifyAll") @PostMapping("/notifyAll")
fun notifyAll(@RequestBody payload: PushMessage): ResponseEntity<Any> { suspend fun notifyAll(@RequestBody payload: PushMessage): ResponseEntity<Any> {
return try { return try {
ResponseEntity.ok(subscriptionService.sendToAll(payload)) ResponseEntity.ok(subscriptionService.sendToAll(payload))
} catch (e: Exception) { } catch (e: Exception) {

View File

@@ -3,6 +3,7 @@ package space.luminic.budgerapp.controllers
import org.springframework.web.bind.annotation.* import org.springframework.web.bind.annotation.*
import space.luminic.budgerapp.models.WishList import space.luminic.budgerapp.models.WishList
import space.luminic.budgerapp.models.WishListItem import space.luminic.budgerapp.models.WishListItem
import space.luminic.budgerapp.services.AuthService
import space.luminic.budgerapp.services.SpaceService import space.luminic.budgerapp.services.SpaceService
import space.luminic.budgerapp.services.WishListService import space.luminic.budgerapp.services.WishListService
@@ -11,24 +12,28 @@ import space.luminic.budgerapp.services.WishListService
@RequestMapping("/spaces/{spaceId}/wishlists") @RequestMapping("/spaces/{spaceId}/wishlists")
class WishListController( class WishListController(
private val wishListService: WishListService, private val wishListService: WishListService,
private val spaceService: SpaceService private val spaceService: SpaceService,
private val authService: AuthService
) { ) {
@GetMapping @GetMapping
suspend fun findWishList(@PathVariable spaceId: String): List<WishList> { suspend fun findWishList(@PathVariable spaceId: String): List<WishList> {
spaceService.isValidRequest(spaceId) val user = authService.getSecurityUser()
spaceService.isValidRequest(spaceId, user)
return wishListService.findWishLists(spaceId) return wishListService.findWishLists(spaceId)
} }
@GetMapping("/{wishListId}") @GetMapping("/{wishListId}")
suspend fun getWishList(@PathVariable spaceId: String, @PathVariable wishListId: String): WishList { suspend fun getWishList(@PathVariable spaceId: String, @PathVariable wishListId: String): WishList {
spaceService.isValidRequest(spaceId) val user = authService.getSecurityUser()
spaceService.isValidRequest(spaceId, user)
return wishListService.getList(wishListId) return wishListService.getList(wishListId)
} }
@PostMapping @PostMapping
suspend fun createWishList(@PathVariable spaceId: String, @RequestBody wishList: WishList): WishList { suspend fun createWishList(@PathVariable spaceId: String, @RequestBody wishList: WishList): WishList {
val space = spaceService.isValidRequest(spaceId) val user = authService.getSecurityUser()
val space = spaceService.isValidRequest(spaceId, user)
return wishListService.createWishList(space, wishList) return wishListService.createWishList(space, wishList)
} }
@@ -38,7 +43,8 @@ class WishListController(
@PathVariable wishListId: String, @PathVariable wishListId: String,
@RequestBody wishList: WishList @RequestBody wishList: WishList
): WishList { ): WishList {
spaceService.isValidRequest(spaceId) val user = authService.getSecurityUser()
spaceService.isValidRequest(spaceId, user)
return wishListService.updateWishListInfo(wishList) return wishListService.updateWishListInfo(wishList)
} }
@@ -49,7 +55,8 @@ class WishListController(
@PathVariable itemId: String, @PathVariable itemId: String,
@RequestBody wishListItem: WishListItem @RequestBody wishListItem: WishListItem
): WishList { ): WishList {
spaceService.isValidRequest(spaceId) val user = authService.getSecurityUser()
spaceService.isValidRequest(spaceId, user)
return wishListService.updateWishListItemInfo(wishListId, wishListItem) return wishListService.updateWishListItemInfo(wishListId, wishListItem)
} }
@@ -59,7 +66,8 @@ class WishListController(
@PathVariable wishListId: String, @PathVariable wishListId: String,
@RequestBody wishListItem: WishListItem @RequestBody wishListItem: WishListItem
): WishList { ): WishList {
spaceService.isValidRequest(spaceId) val user = authService.getSecurityUser()
spaceService.isValidRequest(spaceId, user)
return wishListService.addItemToWishList(wishListId, wishListItem) return wishListService.addItemToWishList(wishListId, wishListItem)
} }
@@ -69,13 +77,15 @@ class WishListController(
@PathVariable wishListId: String, @PathVariable wishListId: String,
@PathVariable wishListItemId: String @PathVariable wishListItemId: String
): WishList { ): WishList {
spaceService.isValidRequest(spaceId) val user = authService.getSecurityUser()
spaceService.isValidRequest(spaceId, user)
return wishListService.removeItemFromWishList(wishListId, wishListItemId) return wishListService.removeItemFromWishList(wishListId, wishListItemId)
} }
@DeleteMapping("/{wishListId}") @DeleteMapping("/{wishListId}")
suspend fun deleteWishList(@PathVariable spaceId: String, @PathVariable wishListId: String) { suspend fun deleteWishList(@PathVariable spaceId: String, @PathVariable wishListId: String) {
spaceService.isValidRequest(spaceId) val user = authService.getSecurityUser()
spaceService.isValidRequest(spaceId, user)
wishListService.deleteWishList(wishListId) wishListService.deleteWishList(wishListId)
} }
@@ -83,8 +93,9 @@ class WishListController(
suspend fun cancelReserve( suspend fun cancelReserve(
@PathVariable spaceId: String, @PathVariable wishListId: String, @PathVariable spaceId: String, @PathVariable wishListId: String,
@PathVariable wishlistItemId: String @PathVariable wishlistItemId: String
) : WishList { ): WishList {
spaceService.isValidRequest(spaceId) val user = authService.getSecurityUser()
spaceService.isValidRequest(spaceId, user)
return wishListService.cancelReserve(wishListId, wishlistItemId) return wishListService.cancelReserve(wishListId, wishlistItemId)
} }
} }

View File

@@ -0,0 +1,22 @@
package space.luminic.budgerapp.models
import org.springframework.data.annotation.Id
import org.springframework.data.mongodb.core.mapping.DBRef
import org.springframework.data.mongodb.core.mapping.Document
enum class BotStates {
WAIT_CATEGORY
}
@Document("bot-user-states")
data class BotUserState(
@Id val id: String? = null,
@DBRef val user: User,
var data: MutableList<ChatData> = mutableListOf(),
)
data class ChatData (
val chatId: String,
var state: BotStates,
var data: MutableMap<String, String> = mutableMapOf(),
)

View File

@@ -1,4 +1,5 @@
package space.luminic.budgerapp.models package space.luminic.budgerapp.models
open class NotFoundException(message: String) : Exception(message) open class NotFoundException(message: String) : Exception(message)
open class TelegramBotException(message: String, val chatId: Long) : Exception(message)

View File

@@ -0,0 +1,7 @@
package space.luminic.budgerapp.repos
import org.springframework.data.mongodb.repository.ReactiveMongoRepository
import space.luminic.budgerapp.models.BotUserState
interface BotStatesRepo: ReactiveMongoRepository<BotUserState, String> {
}

View File

@@ -1,10 +1,21 @@
package space.luminic.budgerapp.repos package space.luminic.budgerapp.repos
import org.bson.types.ObjectId
import org.springframework.data.mongodb.repository.Query
import org.springframework.data.mongodb.repository.ReactiveMongoRepository import org.springframework.data.mongodb.repository.ReactiveMongoRepository
import org.springframework.stereotype.Repository import org.springframework.stereotype.Repository
import reactor.core.publisher.Flux
import space.luminic.budgerapp.models.Subscription import space.luminic.budgerapp.models.Subscription
@Repository @Repository
interface SubscriptionRepo : ReactiveMongoRepository<Subscription, String> { interface SubscriptionRepo : ReactiveMongoRepository<Subscription, String> {
@Query("{ \$and: [ " +
"{ 'user': { '\$ref': 'users', '\$id': ?0 } }, " +
"{ 'isActive': true } " +
"]}")
fun findByUserIdAndIsActive(userId: ObjectId): Flux<Subscription>
} }

View File

@@ -4,10 +4,10 @@ import kotlinx.coroutines.reactive.awaitFirstOrNull
import kotlinx.coroutines.reactor.awaitSingle import kotlinx.coroutines.reactor.awaitSingle
import kotlinx.coroutines.reactor.awaitSingleOrNull import kotlinx.coroutines.reactor.awaitSingleOrNull
import org.springframework.cache.annotation.Cacheable import org.springframework.cache.annotation.Cacheable
import org.springframework.security.core.context.ReactiveSecurityContextHolder
import org.springframework.security.core.userdetails.UsernameNotFoundException import org.springframework.security.core.userdetails.UsernameNotFoundException
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import reactor.core.publisher.Mono
import space.luminic.budgerapp.configs.AuthException import space.luminic.budgerapp.configs.AuthException
import space.luminic.budgerapp.models.TokenStatus import space.luminic.budgerapp.models.TokenStatus
import space.luminic.budgerapp.models.User import space.luminic.budgerapp.models.User
@@ -28,6 +28,16 @@ class AuthService(
) { ) {
private val passwordEncoder = BCryptPasswordEncoder() private val passwordEncoder = BCryptPasswordEncoder()
suspend fun getSecurityUser(): User {
val securityContextHolder = ReactiveSecurityContextHolder.getContext().awaitSingleOrNull()
?: throw AuthException("Authentication failed")
val authentication = securityContextHolder.authentication
val username = authentication.name
// Получаем пользователя по имени
return userService.getByUsername(username)
}
suspend fun login(username: String, password: String): String { suspend fun login(username: String, password: String): String {
val user = userRepository.findByUsername(username).awaitFirstOrNull() val user = userRepository.findByUsername(username).awaitFirstOrNull()
?: throw UsernameNotFoundException("Пользователь не найден") ?: throw UsernameNotFoundException("Пользователь не найден")

View File

@@ -0,0 +1,308 @@
package space.luminic.budgerapp.services
import kotlinx.coroutines.reactive.awaitSingle
import kotlinx.coroutines.reactor.awaitSingleOrNull
import kotlinx.coroutines.runBlocking
import org.bson.Document
import org.bson.types.ObjectId
import org.slf4j.LoggerFactory
import org.springframework.data.mongodb.core.ReactiveMongoTemplate
import org.springframework.data.mongodb.core.aggregation.Aggregation.*
import org.springframework.data.mongodb.core.query.Criteria
import org.springframework.stereotype.Service
import org.telegram.telegrambots.bots.TelegramLongPollingBot
import org.telegram.telegrambots.meta.api.methods.send.SendMessage
import org.telegram.telegrambots.meta.api.methods.updatingmessages.DeleteMessage
import org.telegram.telegrambots.meta.api.methods.updatingmessages.EditMessageText
import org.telegram.telegrambots.meta.api.objects.Update
import org.telegram.telegrambots.meta.api.objects.replykeyboard.InlineKeyboardMarkup
import org.telegram.telegrambots.meta.api.objects.replykeyboard.buttons.InlineKeyboardButton
import org.telegram.telegrambots.meta.exceptions.TelegramApiException
import space.luminic.budgerapp.configs.TelegramBotConfig
import space.luminic.budgerapp.configs.TelegramBotProperties
import space.luminic.budgerapp.models.*
import space.luminic.budgerapp.repos.BotStatesRepo
import java.time.LocalDate
import java.time.LocalDateTime
@Service
class BotService(
private val telegramBotProperties: TelegramBotProperties,
private val botStatesRepo: BotStatesRepo,
private val reactiveMongoTemplate: ReactiveMongoTemplate,
private val userService: UserService,
private val categoriesService: CategoryService,
private val financialService: FinancialService,
private val spaceService: SpaceService,
) : TelegramLongPollingBot(telegramBotProperties.token) {
private val logger = LoggerFactory.getLogger(javaClass)
override fun getBotUsername(): String {
return telegramBotProperties.username
}
override fun onUpdateReceived(update: Update) = runBlocking {
logger.info("Received message $update")
try {
if (update.hasCallbackQuery()) {
processCallback(update)
} else if (update.hasMessage()) {
if (update.message.hasText()) {
processMessage(update)
} else if (update.message.hasPhoto()) {
processPhoto(update)
} else if (update.message.hasVideo()) {
processVideo(update)
}
}
} catch (e: TelegramBotException) {
e.printStackTrace()
logger.error(e.message)
sendMessage(e.chatId.toString(), "${e.message}")
}
}
private suspend fun processCallback(update: Update) {
val chatId = update.callbackQuery.message.chatId.toString()
val tgUserId = update.callbackQuery.from.id
val user = userService.getUserByTelegramId(tgUserId) ?: throw TelegramBotException(
"User ${update.callbackQuery.from.userName} not found",
chatId = update.callbackQuery.message.chatId
)
val state = getState(user.id!!)
if (state != null) {
when (state.data.first { it.chatId == chatId }.state) {
BotStates.WAIT_CATEGORY ->
if (update.callbackQuery.data.startsWith("category_")) {
confirmTransaction(
chatId,
user = userService.getUserByTelegramId(tgUserId)!!,
update
)
} else if (update.callbackQuery.data == "cancel") {
finishState(chatId, user)
val deleteMsg = DeleteMessage(chatId, update.callbackQuery.message.messageId)
execute(deleteMsg)
sendMessage(chatId, "Введите сумму и комментарий когда будете готовы.")
}
}
}
}
private suspend fun processMessage(update: Update) {
val user = userService.getUserByTelegramId(update.message.from.id) ?: throw TelegramBotException(
"Мы не знакомы",
chatId = update.message.chatId,
)
getState(user.id!!)?.data?.find { it.chatId == update.message.chatId.toString() }?.let {
if (it.state == BotStates.WAIT_CATEGORY) {
throw TelegramBotException(
"Уже есть открытый выбор категории",
update.message.chatId
)
}
}
newExpense(
update.message.chatId.toString(),
user = user,
text = update.message.text,
)
}
private fun processPhoto(update: Update) {
}
private fun processVideo(update: Update) {
}
private fun sendMessage(chatId: String, text: String) {
val message = SendMessage(chatId, text)
try {
execute(message)
} catch (e: TelegramApiException) {
e.printStackTrace()
}
}
private suspend fun newExpense(chatId: String, user: User, text: String) {
val splitText = text.split(" ")
if (splitText.size < 2) {
try {
throw TelegramBotException("Сумма или комментарий не введены", chatId.toLong())
} catch (e: TelegramApiException) {
e.printStackTrace()
}
} else {
val sum = try {
splitText[0].toInt()
} catch (e: NumberFormatException) {
throw TelegramBotException("Кажется первый параметр не цифра", chatId.toLong())
}
val textWOSum = splitText.drop(1)
var comment = ""
textWOSum.map { word ->
comment += "$word "
}
val categories =
categoriesService.getCategories(
"67af3c0f652da946a7dd9931",
"EXPENSE",
sortBy = "name",
direction = "ASC"
)
.awaitSingle()
val keyboard = InlineKeyboardMarkup()
val buttonLines = mutableListOf<MutableList<InlineKeyboardButton>>()
categories.map { category ->
val btn = InlineKeyboardButton.builder().text("${category.icon} ${category.name}")
.callbackData("category_${category.id}").build()
if (category.name.length >= 15) {
// Если текст длинный, создаём отдельную строку для кнопки
buttonLines.add(mutableListOf(btn))
} else {
var isAdded = false
// Пытаемся добавить кнопку в существующую строку
for (line in buttonLines) {
if (line.size < 2 && (line.isEmpty() || line[0].text.length < 14)) {
line.add(btn)
isAdded = true
break
}
}
// Если не нашли подходящую строку, создаём новую
if (!isAdded) {
buttonLines.add(mutableListOf(btn))
} else {
}
}
}
val backButton = InlineKeyboardButton.builder().text("Отмена").callbackData("cancel").build()
buttonLines.add(mutableListOf(backButton))
keyboard.keyboard = buttonLines
val message = SendMessage()
message.chatId = chatId
val msg = "Выберите категорию"
message.text = msg
message.replyMarkup = keyboard
val userState = BotUserState(user = user)
val chatData =
userState.data.find { it.chatId == chatId } ?: ChatData(chatId, state = BotStates.WAIT_CATEGORY)
chatData.data["sum"] = sum.toString()
chatData.data["comment"] = comment
userState.data.add(chatData)
try {
execute(message)
setState(userState)
} catch (e: TelegramApiException) {
e.printStackTrace()
}
}
}
private suspend fun confirmTransaction(chatId: String, user: User, update: Update) {
val state = getState(user.id!!)
if (state == null) {
sendMessage(chatId, "Не можем найти информацию о сумме и комментарии")
return
}
val stateData = state.data.find { it.chatId == chatId }
if (stateData == null) {
sendMessage(chatId, "Не можем найти информацию о сумме и комментарии")
return
}
val category = categoriesService.getCategories(
"67af3c0f652da946a7dd9931",
"EXPENSE",
sortBy = "name",
direction = "ASC"
)
.awaitSingle()
.first { it.id == update.callbackQuery.data.split("_")[1] }
val space = spaceService.getSpace("67af3c0f652da946a7dd9931")
val instantType = financialService.getTransactionTypes().first { it.code == "INSTANT" }
val transaction = financialService.createTransaction(
space,
transaction = Transaction(
space = space,
type = instantType,
user = user,
category = category,
comment = stateData.data["comment"]!!.trim(),
date = LocalDate.now(),
amount = stateData.data["sum"]!!.toDouble(),
isDone = true,
parentId = null,
createdAt = LocalDateTime.now()
),
user = user
)
val editMsg = EditMessageText()
editMsg.chatId = chatId
editMsg.messageId = update.callbackQuery.message.messageId
editMsg.text = "Успешно создали транзакцию c id ${transaction.id}"
try {
execute(editMsg)
// execute(msg)
finishState(chatId, user)
} catch (e: TelegramApiException) {
e.printStackTrace()
logger.error(e.message)
}
}
private suspend fun getState(userId: String): BotUserState? {
val lookup = lookup("users", "user.\$id", "_id", "userDetails")
val unwind = unwind("userDetails")
val match = match(Criteria.where("userDetails._id").`is`(ObjectId(userId)))
val aggregation = newAggregation(lookup, unwind, match)
return reactiveMongoTemplate.aggregate(aggregation, "bot-user-states", Document::class.java)
.next()
.map { doc ->
val dataList = doc.getList("data", Document::class.java)
BotUserState(
id = doc.getObjectId("_id").toString(),
user = User(doc.get("userDetails", Document::class.java).getObjectId("_id").toString()),
data = dataList.map {
val data = it.get("data", Document::class.java)
ChatData(
chatId = it.getString("chatId"),
state = BotStates.valueOf(it.getString("state")),
data = (data.toMap().mapValues { it.value.toString() }.toMutableMap())
)
}.toMutableList(),
)
}
.awaitSingleOrNull()
}
private suspend fun setState(userState: BotUserState): BotUserState {
val stateToSave = userState.user.id?.let { userId ->
getState(userId)?.copy(data = userState.data)
?: BotUserState(user = userState.user, data = userState.data)
} ?: BotUserState(user = userState.user, data = userState.data)
return botStatesRepo.save(stateToSave).awaitSingle()
}
private suspend fun finishState(chatId: String, user: User) {
val state = getState(user.id!!)
state?.data?.removeIf { it.chatId == chatId }
state?.let { botStatesRepo.save(state).awaitSingle() }
}
}

View File

@@ -17,10 +17,7 @@ import org.springframework.data.mongodb.core.query.isEqualTo
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import reactor.core.publisher.Mono import reactor.core.publisher.Mono
import space.luminic.budgerapp.mappers.CategoryMapper import space.luminic.budgerapp.mappers.CategoryMapper
import space.luminic.budgerapp.models.Category import space.luminic.budgerapp.models.*
import space.luminic.budgerapp.models.CategoryType
import space.luminic.budgerapp.models.NotFoundException
import space.luminic.budgerapp.models.Space
import space.luminic.budgerapp.repos.BudgetRepo import space.luminic.budgerapp.repos.BudgetRepo
import space.luminic.budgerapp.repos.CategoryRepo import space.luminic.budgerapp.repos.CategoryRepo
@@ -133,7 +130,7 @@ class CategoryService(
return categoryRepo.save(category).awaitSingle() // Сохраняем категорию, если тип не изменился return categoryRepo.save(category).awaitSingle() // Сохраняем категорию, если тип не изменился
} }
suspend fun deleteCategory(space: Space, categoryId: String) { suspend fun deleteCategory(space: Space, categoryId: String, author: User) {
findCategory(space, categoryId) findCategory(space, categoryId)
val transactions = financialService.getTransactions(space.id!!, categoryId = categoryId).awaitSingle() val transactions = financialService.getTransactions(space.id!!, categoryId = categoryId).awaitSingle()
if (transactions.isNotEmpty()) { if (transactions.isNotEmpty()) {
@@ -154,7 +151,7 @@ class CategoryService(
transactions.map { transaction -> transactions.map { transaction ->
transaction.category = otherCategory transaction.category = otherCategory
financialService.editTransaction(transaction) financialService.editTransaction(transaction, author)
} }
} }
val budgets = financialService.findProjectedBudgets( val budgets = financialService.findProjectedBudgets(

View File

@@ -1,11 +1,8 @@
package space.luminic.budgerapp.services package space.luminic.budgerapp.services
import kotlinx.coroutines.async import kotlinx.coroutines.*
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.toList import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.launch
import kotlinx.coroutines.reactive.asFlow import kotlinx.coroutines.reactive.asFlow
import kotlinx.coroutines.reactive.awaitFirstOrNull import kotlinx.coroutines.reactive.awaitFirstOrNull
import kotlinx.coroutines.reactive.awaitSingle import kotlinx.coroutines.reactive.awaitSingle
@@ -35,6 +32,7 @@ import space.luminic.budgerapp.repos.CategoryRepo
import space.luminic.budgerapp.repos.TransactionRepo import space.luminic.budgerapp.repos.TransactionRepo
import space.luminic.budgerapp.repos.WarnRepo import space.luminic.budgerapp.repos.WarnRepo
import java.time.* import java.time.*
import java.time.format.DateTimeFormatter
import java.time.temporal.TemporalAdjusters import java.time.temporal.TemporalAdjusters
import java.util.* import java.util.*
@@ -48,7 +46,8 @@ class FinancialService(
val reactiveMongoTemplate: ReactiveMongoTemplate, val reactiveMongoTemplate: ReactiveMongoTemplate,
private val categoryRepo: CategoryRepo, private val categoryRepo: CategoryRepo,
val transactionsMapper: TransactionsMapper, val transactionsMapper: TransactionsMapper,
val budgetMapper: BudgetMapper val budgetMapper: BudgetMapper,
private val subscriptionService: SubscriptionService
) { ) {
private val logger = LoggerFactory.getLogger(FinancialService::class.java) private val logger = LoggerFactory.getLogger(FinancialService::class.java)
@@ -846,25 +845,45 @@ class FinancialService(
} }
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
suspend fun createTransaction(space: Space, transaction: Transaction): Transaction { suspend fun createTransaction(space: Space, transaction: Transaction, user: User? = null): Transaction {
val securityContextHolder = ReactiveSecurityContextHolder.getContext().awaitSingle() val author = user
val user = userService.getByUserNameWoPass(securityContextHolder.authentication.name) ?: userService.getByUserNameWoPass(
if (space.users.none { it.id.toString() == user.id }) { ReactiveSecurityContextHolder.getContext().awaitSingle().authentication.name
)
if (space.users.none { it.id.toString() == author.id }) {
throw IllegalArgumentException("User does not have access to this Space") throw IllegalArgumentException("User does not have access to this Space")
} }
// Привязываем space и user к транзакции // Привязываем space и user к транзакции
transaction.user = user transaction.user = author
transaction.space = space transaction.space = space
val savedTransaction = transactionsRepo.save(transaction).awaitSingle() val savedTransaction = transactionsRepo.save(transaction).awaitSingle()
updateBudgetOnCreate(savedTransaction) updateBudgetOnCreate(savedTransaction)
scope.launch {
val dateFormatter = DateTimeFormatter.ofPattern("dd.MM.yyyy")
val transactionType = if (transaction.type.code == "INSTANT") "текущую" else "плановую"
subscriptionService.sendToSpaceOwner(
space.owner!!.id!!, PushMessage(
title = "Новая транзакция в пространстве ${space.name}!",
body = "Пользователь ${author.username} создал $transactionType транзакцию на сумму ${transaction.amount.toInt()} с комментарием ${transaction.comment} с датой ${
dateFormatter.format(
transaction.date
)
}",
url = "https://luminic.space/"
)
)
}
return savedTransaction return savedTransaction
} }
@CacheEvict(cacheNames = ["transactions", "budgets"], allEntries = true) @CacheEvict(cacheNames = ["transactions", "budgets"], allEntries = true)
suspend fun editTransaction(transaction: Transaction): Transaction { suspend fun editTransaction(transaction: Transaction, author: User): Transaction {
val oldStateOfTransaction = getTransactionById(transaction.id!!) val oldStateOfTransaction = getTransactionById(transaction.id!!)
val changed = compareSumDateDoneIsChanged(oldStateOfTransaction, transaction) val changed = compareSumDateDoneIsChanged(oldStateOfTransaction, transaction)
if (!changed) { if (!changed) {
@@ -878,8 +897,57 @@ class FinancialService(
transaction transaction
) )
} }
val space = transaction.space
val savedTransaction = transactionsRepo.save(transaction).awaitSingle() val savedTransaction = transactionsRepo.save(transaction).awaitSingle()
updateBudgetOnEdit(oldStateOfTransaction, savedTransaction, amountDifference) updateBudgetOnEdit(oldStateOfTransaction, savedTransaction, amountDifference)
scope.launch {
var whatChanged = "nothing"
val dateFormatter = DateTimeFormatter.ofPattern("dd.MM.yyyy")
val transactionType = if (transaction.type.code == "INSTANT") "текущую" else "плановую"
val sb = StringBuilder()
if (oldStateOfTransaction.amount != transaction.amount) {
sb.append("${oldStateOfTransaction.amount}${transaction.amount}\n")
whatChanged = "main"
}
if (oldStateOfTransaction.comment != transaction.comment) {
sb.append("${oldStateOfTransaction.comment}${transaction.comment}\n")
whatChanged = "main"
}
if (oldStateOfTransaction.date != transaction.date) {
sb.append("${dateFormatter.format(oldStateOfTransaction.date)}${dateFormatter.format(transaction.date)}\n")
whatChanged = "main"
}
if (!oldStateOfTransaction.isDone && transaction.isDone) {
whatChanged = "done_true"
}
if (oldStateOfTransaction.isDone && !transaction.isDone) {
whatChanged = "done_false"
}
val body: String = when (whatChanged) {
"main" -> {
"Пользователь ${author.username} изменил $transactionType транзакцию:\n$sb"
}
"done_true" -> {
"Пользователь ${author.username} выполнил ${transaction.comment} с суммой ${transaction.amount.toInt()}"
}
"done_false" -> {
"Пользователь ${author.username} отменил выполнение ${transaction.comment} с суммой ${transaction.amount.toInt()}"
}
else -> "Изменения не обнаружены, но что то точно изменилось"
}
subscriptionService.sendToSpaceOwner(
space?.owner!!.id!!, PushMessage(
title = "Новое действие в пространстве ${space.name}!",
body = body,
url = "https://luminic.space/"
)
)
}
return savedTransaction return savedTransaction
} }

View File

@@ -37,14 +37,9 @@ class SpaceService(
private val tagRepo: TagRepo private val tagRepo: TagRepo
) { ) {
suspend fun isValidRequest(spaceId: String): Space {
val securityContextHolder = ReactiveSecurityContextHolder.getContext().awaitSingleOrNull()
?: throw AuthException("Authentication failed")
val authentication = securityContextHolder.authentication
val username = authentication.name
// Получаем пользователя по имени suspend fun isValidRequest(spaceId: String, user: User): Space {
val user = userService.getByUsername(username)
val space = getSpace(spaceId) val space = getSpace(spaceId)
// Проверяем доступ пользователя к пространству // Проверяем доступ пользователя к пространству

View File

@@ -3,13 +3,16 @@ package space.luminic.budgerapp.services
import com.interaso.webpush.VapidKeys import com.interaso.webpush.VapidKeys
import com.interaso.webpush.WebPushService import com.interaso.webpush.WebPushService
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.reactive.awaitSingle import kotlinx.coroutines.reactive.awaitSingle
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import org.bson.types.ObjectId
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.springframework.dao.DuplicateKeyException import org.springframework.dao.DuplicateKeyException
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import reactor.core.publisher.Mono
import space.luminic.budgerapp.models.PushMessage import space.luminic.budgerapp.models.PushMessage
import space.luminic.budgerapp.models.Subscription import space.luminic.budgerapp.models.Subscription
import space.luminic.budgerapp.models.SubscriptionDTO import space.luminic.budgerapp.models.SubscriptionDTO
@@ -36,38 +39,50 @@ class SubscriptionService(private val subscriptionRepo: SubscriptionRepo) {
vapidKeys = VapidKeys.fromUncompressedBytes(VAPID_PUBLIC_KEY, VAPID_PRIVATE_KEY) vapidKeys = VapidKeys.fromUncompressedBytes(VAPID_PUBLIC_KEY, VAPID_PRIVATE_KEY)
) )
fun sendNotification(endpoint: String, p256dh: String, auth: String, payload: PushMessage): Mono<Void> {
return Mono.fromRunnable<Void> { suspend fun sendToSpaceOwner(ownerId: String, message: PushMessage) = coroutineScope {
val ownerTokens = subscriptionRepo.findByUserIdAndIsActive(ObjectId(ownerId)).collectList().awaitSingle()
ownerTokens.forEach { token ->
launch(Dispatchers.IO) { // Теперь мы точно в корутин скоупе
try {
sendNotification(token.endpoint, token.p256dh, token.auth, message)
} catch (e: Exception) {
logger.error("Ошибка при отправке уведомления: ${e.message}", e)
}
}
}
}
suspend fun sendNotification(endpoint: String, p256dh: String, auth: String, payload: PushMessage) {
try {
pushService.send( pushService.send(
payload = Json.encodeToString(payload), payload = Json.encodeToString(payload),
endpoint = endpoint, endpoint = endpoint,
p256dh = p256dh, p256dh = p256dh,
auth = auth auth = auth
) )
logger.info("Уведомление успешно отправлено на endpoint: $endpoint")
} catch (e: Exception) {
logger.error("Ошибка при отправке уведомления на endpoint $endpoint: ${e.message}")
throw e
} }
.doOnSuccess {
logger.info("Уведомление успешно отправлено на endpoint: $endpoint")
}
.doOnError { e ->
logger.error("Ошибка при отправке уведомления на endpoint $endpoint: ${e.message}")
}
.onErrorResume { e ->
Mono.error(e) // Пробрасываем ошибку дальше, если нужна обработка выше
}
} }
fun sendToAll(payload: PushMessage): Mono<List<String>> { suspend fun sendToAll(payload: PushMessage) {
return subscriptionRepo.findAll()
.flatMap { sub -> subscriptionRepo.findAll().collectList().awaitSingle().forEach { sub ->
try {
sendNotification(sub.endpoint, sub.p256dh, sub.auth, payload) sendNotification(sub.endpoint, sub.p256dh, sub.auth, payload)
.then(Mono.just("${sub.user?.username} at endpoint ${sub.endpoint}")) } catch (e: Exception) {
.onErrorResume { e -> sub.isActive = false
sub.isActive = false subscriptionRepo.save(sub).awaitSingle()
subscriptionRepo.save(sub).then(Mono.empty())
}
} }
.collectList() // Собираем результаты в список }
} }

View File

@@ -29,8 +29,12 @@ class UserService(val userRepo: UserRepo) {
.switchIfEmpty(Mono.error(Exception("User not found"))) // Обрабатываем случай, когда пользователь не найден .switchIfEmpty(Mono.error(Exception("User not found"))) // Обрабатываем случай, когда пользователь не найден
} }
suspend fun getUserByTelegramId(telegramId: Long): User? {
return userRepo.findByTgId(telegramId.toString()).awaitSingleOrNull()
}
@Cacheable("users", key = "#username")
@Cacheable("users", key = "#username")
suspend fun getByUserNameWoPass(username: String): User { suspend fun getByUserNameWoPass(username: String): User {
return userRepo.findByUsernameWOPassword(username).awaitSingleOrNull() return userRepo.findByUsernameWOPassword(username).awaitSingleOrNull()
?: throw NotFoundException("User with username: $username not found") ?: throw NotFoundException("User with username: $username not found")

View File

@@ -14,7 +14,7 @@ class ScheduledTasks(private val subscriptionService: SubscriptionService,
private val logger = LoggerFactory.getLogger(ScheduledTasks::class.java) private val logger = LoggerFactory.getLogger(ScheduledTasks::class.java)
@Scheduled(cron = "0 30 19 * * *") @Scheduled(cron = "0 30 19 * * *")
fun sendNotificationOfMoneyFilling() { suspend fun sendNotificationOfMoneyFilling() {
subscriptionService.sendToAll( subscriptionService.sendToAll(
PushMessage( PushMessage(
title = "Время заполнять траты!🤑", title = "Время заполнять траты!🤑",
@@ -23,13 +23,6 @@ class ScheduledTasks(private val subscriptionService: SubscriptionService,
badge = "/apple-touch-icon.png", badge = "/apple-touch-icon.png",
url = "https://luminic.space/transactions/create" url = "https://luminic.space/transactions/create"
) )
).doOnNext { responses ->
responses.forEach { response ->
logger.info("Уведомление отправлено: $response")
}
}.subscribe(
{ logger.info("Все уведомления отправлены.") },
{ error -> logger.error("Ошибка при отправке уведомлений: ${error.message}", error) }
) )
} }

View File

@@ -18,10 +18,4 @@ spring.datasource.username=familybudget_app
spring.datasource.password=FB1q2w3e4r! spring.datasource.password=FB1q2w3e4r!
# ??????? JDBC telegram.bot.token = 6972242509:AAGyXuL3T-BNE4XMoo_qvtaYxw_SuiS_dDs
spring.datasource.driver-class-name=org.postgresql.Driver
# ????????? Hibernate
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect
spring.jpa.hibernate.ddl-auto=none

View File

@@ -1,27 +1,8 @@
spring.application.name=budger-app spring.application.name=budger-app
spring.data.mongodb.uri=mongodb://budger-app:BA1q2w3e4r!@luminic.space:27017/budger-app?authSource=admin&minPoolSize=10&maxPoolSize=100
spring.data.mongodb.host=77.222.32.64
spring.data.mongodb.port=27017
spring.data.mongodb.database=budger-app
#spring.data.mongodb.username=budger-app #spring.data.mongodb.username=budger-app
#spring.data.mongodb.password=BA1q2w3e4r! #spring.data.mongodb.password=BA1q2w3e4r!
#spring.data.mongodb.authentication-database=admin #spring.data.mongodb.authentication-database=admin
management.endpoints.web.exposure.include=* management.endpoints.web.exposure.include=*
management.endpoint.health.show-details=always management.endpoint.health.show-details=always
telegram.bot.token=6972242509:AAGyXuL3T-BNE4XMoo_qvtaYxw_SuiS_dDs
spring.datasource.url=jdbc:postgresql://213.183.51.243/familybudget_app
spring.datasource.username=familybudget_app
spring.datasource.password=FB1q2w3e4r!
# ??????? JDBC
spring.datasource.driver-class-name=org.postgresql.Driver
# ????????? Hibernate
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect
spring.jpa.hibernate.ddl-auto=none

View File

@@ -16,5 +16,5 @@ logging.level.org.springframework.data.mongodb.code = DEBUG
#management.endpoint.metrics.access=read_only #management.endpoint.metrics.access=read_only
telegram.bot.token = 6662300972:AAFXjk_h0AUCy4bORC12UcdXbYnh2QSVKAY

View File

@@ -34,3 +34,6 @@ management.endpoints.web.exposure.include=*
# Enable Prometheus metrics export # Enable Prometheus metrics export
management.prometheus.metrics.export.enabled=true management.prometheus.metrics.export.enabled=true
telegram.bot.username = expenses_diary_bot