Skip to content

Commit

Permalink
Add support for admin /stats command
Browse files Browse the repository at this point in the history
  • Loading branch information
ekuzmichev committed Oct 28, 2024
1 parent 9d0caa1 commit e90e461
Show file tree
Hide file tree
Showing 8 changed files with 69 additions and 37 deletions.
5 changes: 5 additions & 0 deletions src/main/scala/bot/BotAdminCommands.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package ru.ekuzmichev
package bot

object BotAdminCommands:
val Stats = "/stats"
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package ru.ekuzmichev
package bot

object OzonPriceCheckerBotCommands:
object BotGeneralCommands:
val Cancel = "/cancel"
val Start = "/start"
val Stop = "/stop"
Expand Down
3 changes: 2 additions & 1 deletion src/main/scala/config/AppConfig.scala
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,6 @@ case class AppConfig(
botToken: Sensitive[String],
priceCheckingCron: String,
logBotStatusInterval: Duration,
cacheStateFilePath: String
cacheStateFilePath: String,
admins: Seq[Sensitive[String]]
) extends NamedToString
11 changes: 8 additions & 3 deletions src/main/scala/config/DecryptingAppConfigProvider.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,21 @@ package config
import common.Sensitive
import encryption.EncDec

import zio.Task
import zio.{Task, ZIO}

class DecryptingAppConfigProvider(decoratee: AppConfigProvider, encDec: EncDec) extends AppConfigProvider:
override def provideAppConfig(): Task[AppConfig] =
decoratee
.provideAppConfig()
.flatMap { appConfig =>
decrypt(appConfig.botToken)
.map(decryptedBotToken => appConfig.copy(botToken = decryptedBotToken))
for
decryptedBotToken <- decrypt(appConfig.botToken)
decryptedAdmins <- decrypt(appConfig.admins)
yield appConfig.copy(botToken = decryptedBotToken)
}

private def decrypt(sensitive: Sensitive[String]): Task[Sensitive[String]] =
encDec.decrypt(sensitive.value).map(Sensitive.apply)

private def decrypt(sensitives: Seq[Sensitive[String]]): Task[Seq[Sensitive[String]]] =
ZIO.foreach(sensitives)(decrypt)
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ package ru.ekuzmichev
package consumer

import bot.CallbackData.DeleteProduct
import bot.{CallbackData, OzonPriceCheckerBotCommands}
import bot.{BotAdminCommands, BotGeneralCommands, CallbackData}
import store.ProductStore
import store.ProductStore.ProductCandidate.WaitingProductId
import store.ProductStore.SourceId
import store.ProductStore.{SourceId, SourceState}
import util.telegram.MessageSendingUtils.sendTextMessage
import util.zio.ZioLoggingImplicits.Ops

Expand All @@ -16,19 +16,20 @@ import org.telegram.telegrambots.meta.api.objects.replykeyboard.buttons.{InlineK
import org.telegram.telegrambots.meta.generics.TelegramClient
import zio.{Task, ZIO}

class OzonPriceCheckerCommandProcessor(productStore: ProductStore, telegramClient: TelegramClient)
class BotCommandProcessor(productStore: ProductStore, telegramClient: TelegramClient, admins: Seq[String])
extends CommandProcessor:

private implicit val _telegramClient: TelegramClient = telegramClient

def processCommand(sourceId: SourceId, text: String): Task[Unit] =
if text == OzonPriceCheckerBotCommands.Start then processStartCommand(sourceId)
else if text == OzonPriceCheckerBotCommands.Stop then processStopCommand(sourceId)
else if text == OzonPriceCheckerBotCommands.Cancel then processCancelCommand(sourceId)
else if text == OzonPriceCheckerBotCommands.WatchNewProduct then processWatchNewProductCommand(sourceId)
else if text == OzonPriceCheckerBotCommands.UnwatchProduct then processUnwatchProductCommand(sourceId)
else if text == OzonPriceCheckerBotCommands.UnwatchAllProducts then processUnwatchAllProductsCommand(sourceId)
else if text == OzonPriceCheckerBotCommands.ShowAllProducts then processShowAllProductsCommand(sourceId)
if text == BotGeneralCommands.Start then processStartCommand(sourceId)
else if text == BotGeneralCommands.Stop then processStopCommand(sourceId)
else if text == BotGeneralCommands.Cancel then processCancelCommand(sourceId)
else if text == BotGeneralCommands.WatchNewProduct then processWatchNewProductCommand(sourceId)
else if text == BotGeneralCommands.UnwatchProduct then processUnwatchProductCommand(sourceId)
else if text == BotGeneralCommands.UnwatchAllProducts then processUnwatchAllProductsCommand(sourceId)
else if text == BotGeneralCommands.ShowAllProducts then processShowAllProductsCommand(sourceId)
else if text == BotAdminCommands.Stats && admins.contains(sourceId.userName) then processStatsCommand(sourceId)
else
ZIO.log(s"Got unknown command $text") *>
sendTextMessage(sourceId.chatId, s"I can not process command $text. Please send me a command known to me.")
Expand All @@ -41,7 +42,7 @@ class OzonPriceCheckerCommandProcessor(productStore: ProductStore, telegramClien
else "You have been already added to the Store before."
sendTextMessage(sourceId.chatId, msg)
)
.logged(s"process command ${OzonPriceCheckerBotCommands.Start} ")
.logged(s"process command ${BotGeneralCommands.Start} ")

private def initializeStoreEntry(sourceId: SourceId): Task[Boolean] =
for
Expand All @@ -63,14 +64,14 @@ class OzonPriceCheckerCommandProcessor(productStore: ProductStore, telegramClien
else "You have been already removed from the Store before."
_ <- sendTextMessage(sourceId.chatId, msg)
yield ())
.logged(s"process command ${OzonPriceCheckerBotCommands.Stop} ")
.logged(s"process command ${BotGeneralCommands.Stop} ")

private def processCancelCommand(sourceId: SourceId): Task[Unit] =
productStore.resetProductCandidate(sourceId) *>
sendTextMessage(
sourceId.chatId,
"Current product addition has been cancelled.\n\n" +
s"Send me ${OzonPriceCheckerBotCommands.WatchNewProduct} to start over"
s"Send me ${BotGeneralCommands.WatchNewProduct} to start over"
)

private def processWatchNewProductCommand(sourceId: SourceId): Task[Unit] =
Expand All @@ -82,13 +83,13 @@ class OzonPriceCheckerCommandProcessor(productStore: ProductStore, telegramClien
productStore.updateProductCandidate(sourceId, WaitingProductId)
else askToSendStartCommand(sourceId)
yield ())
.logged(s"process command ${OzonPriceCheckerBotCommands.WatchNewProduct}")
.logged(s"process command ${BotGeneralCommands.WatchNewProduct}")

private def processUnwatchAllProductsCommand(sourceId: SourceId): Task[Unit] =
productStore
.emptyState(sourceId)
.zipRight(sendTextMessage(sourceId.chatId, "I have removed all watched products."))
.logged(s"process command ${OzonPriceCheckerBotCommands.UnwatchAllProducts}")
.logged(s"process command ${BotGeneralCommands.UnwatchAllProducts}")

private def processUnwatchProductCommand(sourceId: SourceId): Task[Unit] =
productStore
Expand All @@ -97,7 +98,7 @@ class OzonPriceCheckerCommandProcessor(productStore: ProductStore, telegramClien
case Some(sourceState) => replyWithInnerDeletionKeyboard(sourceId, sourceState.products)
case None => askToSendStartCommand(sourceId)
}
.logged(s"process command ${OzonPriceCheckerBotCommands.UnwatchProduct}")
.logged(s"process command ${BotGeneralCommands.UnwatchProduct}")

private def replyWithInnerDeletionKeyboard(sourceId: SourceId, products: Seq[ProductStore.Product]): Task[Unit] =
val rows: Seq[InlineKeyboardRow] = products.zipWithIndex.map { case (product, index) =>
Expand Down Expand Up @@ -139,7 +140,7 @@ class OzonPriceCheckerCommandProcessor(productStore: ProductStore, telegramClien
sendTextMessage(
sourceId.chatId,
s"You have no watched products.\n\n" +
s"To watch new product send me ${OzonPriceCheckerBotCommands.WatchNewProduct}"
s"To watch new product send me ${BotGeneralCommands.WatchNewProduct}"
)
case products =>
sendTextMessage(
Expand All @@ -155,11 +156,27 @@ class OzonPriceCheckerCommandProcessor(productStore: ProductStore, telegramClien
case None =>
askToSendStartCommand(sourceId)
}
.logged(s"process command ${OzonPriceCheckerBotCommands.ShowAllProducts}")
.logged(s"process command ${BotGeneralCommands.ShowAllProducts}")
.unit

private def askToSendStartCommand(sourceId: SourceId) =
sendTextMessage(
sourceId.chatId,
s"You have not been yet initialized. Send me ${OzonPriceCheckerBotCommands.Start} command to fix it."
s"You have not been yet initialized. Send me ${BotGeneralCommands.Start} command to fix it."
)

private def processStatsCommand(sourceId: SourceId): Task[Unit] =
productStore
.readAll()
.tap { (sourceStateBySourceId: Map[SourceId, SourceState]) =>
sendTextMessage(
sourceId.chatId,
s"Here are bot user statistics:\n\n" +
s"${sourceStateBySourceId.zipWithIndex
.map { case ((sourceId, sourceState), index) =>
s"${index + 1}) ${sourceId.userName} - ${sourceState.products.size} products"
}
.mkString("\n")}"
)
}
.unit
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package ru.ekuzmichev
package consumer

import bot.CallbackData.DeleteProduct
import bot.{CallbackData, OzonPriceCheckerBotCommands}
import bot.{BotGeneralCommands, CallbackData}
import common.{ChatId, ProductId, UserName}
import product.{ProductFetcher, ProductIdParser}
import store.*
Expand All @@ -18,7 +18,7 @@ import org.telegram.telegrambots.meta.api.objects.{CallbackQuery, Update}
import org.telegram.telegrambots.meta.generics.TelegramClient
import zio.{LogAnnotation, Runtime, Task, ZIO}

class OzonPriceCheckerUpdateConsumer(
class BotUpdateConsumer(
telegramClient: TelegramClient,
productStore: ProductStore,
productFetcher: ProductFetcher,
Expand Down Expand Up @@ -83,7 +83,7 @@ class OzonPriceCheckerUpdateConsumer(
sendTextMessage(
sourceId.chatId,
s"Product '$productId' price $productPrice ₽ is already <= price threshold $priceThreshold ₽.\n\n" +
s"Send new price threshold < $productPrice ₽ or ${OzonPriceCheckerBotCommands.Cancel}"
s"Send new price threshold < $productPrice ₽ or ${BotGeneralCommands.Cancel}"
)
case None =>
sendTextMessage(sourceId.chatId, "Send me valid price as a number")
Expand All @@ -100,7 +100,7 @@ class OzonPriceCheckerUpdateConsumer(
sendTextMessage(
sourceId.chatId,
s"You have already added product with ID $productId.\n\n" +
s"Send me another product ID or command ${OzonPriceCheckerBotCommands.Cancel}"
s"Send me another product ID or command ${BotGeneralCommands.Cancel}"
)
case false =>
productFetcher
Expand All @@ -121,7 +121,7 @@ class OzonPriceCheckerUpdateConsumer(
sendTextMessage(
sourceId.chatId,
s"I can not retrieve information about product with ID $productId.\n\n" +
s"Send me another product ID or command ${OzonPriceCheckerBotCommands.Cancel}"
s"Send me another product ID or command ${BotGeneralCommands.Cancel}"
)
}

Expand All @@ -147,7 +147,7 @@ class OzonPriceCheckerUpdateConsumer(
sendTextMessage(
chatId,
s"The product at index $productIndex has been removed. \n\n" +
s"Check the actual list of watched products with ${OzonPriceCheckerBotCommands.ShowAllProducts}"
s"Check the actual list of watched products with ${BotGeneralCommands.ShowAllProducts}"
)
}
.unit
Expand All @@ -160,9 +160,9 @@ class OzonPriceCheckerUpdateConsumer(
s"ID: $productId\n\n" +
s"Price Threshold: $priceThreshold\n\n" +
s"Added product to watches.\n\n" +
s"To watch new product send me ${OzonPriceCheckerBotCommands.WatchNewProduct}"
s"To watch new product send me ${BotGeneralCommands.WatchNewProduct}"
) *>
productStore.resetProductCandidate(sourceId) *>
productStore.addProduct(sourceId, Product(productId, priceThreshold))

end OzonPriceCheckerUpdateConsumer
end BotUpdateConsumer
11 changes: 8 additions & 3 deletions src/main/scala/consumer/CommandProcessorLayers.scala
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
package ru.ekuzmichev
package consumer

import config.AppConfig
import store.ProductStore

import org.telegram.telegrambots.meta.generics.TelegramClient
import zio.{RLayer, ZLayer}
import zio.{RLayer, ZIO, ZLayer}

object CommandProcessorLayers:
val ozonPriceChecker: RLayer[ProductStore & TelegramClient, CommandProcessor] =
ZLayer.fromFunction(new OzonPriceCheckerCommandProcessor(_, _))
val ozonPriceChecker: RLayer[ProductStore & TelegramClient & AppConfig, CommandProcessor] =
ZLayer.fromZIO(for
productStore <- ZIO.service[ProductStore]
telegramClient <- ZIO.service[TelegramClient]
appConfig <- ZIO.service[AppConfig]
yield new BotCommandProcessor(productStore, telegramClient, appConfig.admins.map(_.value)))
3 changes: 1 addition & 2 deletions src/main/scala/consumer/UpdateConsumerLayers.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package ru.ekuzmichev
package consumer

import product.{ProductFetcher, ProductIdParser}
import schedule.ZioScheduler
import store.ProductStore

import org.telegram.telegrambots.longpolling.interfaces.LongPollingUpdateConsumer
Expand All @@ -23,7 +22,7 @@ object UpdateConsumerLayers:
productIdParser <- ZIO.service[ProductIdParser]
commandProcessor <- ZIO.service[CommandProcessor]
runtime <- ZIO.runtime[Any]
yield new OzonPriceCheckerUpdateConsumer(
yield new BotUpdateConsumer(
telegramClient,
productStore,
productFetcher,
Expand Down

0 comments on commit e90e461

Please sign in to comment.