Skip to content

Commit

Permalink
Implement /unwatchproduct processing with simple logging of callback …
Browse files Browse the repository at this point in the history
…data
  • Loading branch information
ekuzmichev committed Sep 18, 2024
1 parent c218d08 commit c2e8e88
Show file tree
Hide file tree
Showing 10 changed files with 108 additions and 11 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ out/
### Local Config Files ###
*.local.conf

### Local Store Files ###
*.local.store

### Logback ###
logback.xml

Expand Down
7 changes: 2 additions & 5 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,6 @@ ThisBuild / version := "0.1.0-SNAPSHOT"
ThisBuild / organization := "ru.ekuzmichev"
ThisBuild / scalaVersion := "3.3.3"

val zioVersion = "4.0.2"
val circeVersion = "0.14.1"

ThisBuild / assemblyMergeStrategy := {
case PathList("app.local.conf") => MergeStrategy.discard
case PathList("logback.xml") => MergeStrategy.discard
Expand Down Expand Up @@ -43,10 +40,10 @@ lazy val root = (project in file("."))
"dev.zio" %% "zio-config",
"dev.zio" %% "zio-config-magnolia",
"dev.zio" %% "zio-config-typesafe"
).map(_ % zioVersion) ++
).map(_ % "4.0.2") ++
Seq(
"io.circe" %% "circe-core",
"io.circe" %% "circe-generic",
"io.circe" %% "circe-parser"
).map(_ % circeVersion)
).map(_ % "0.14.1")
)
24 changes: 24 additions & 0 deletions src/main/scala/bot/CallbackData.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package ru.ekuzmichev
package bot

import io.circe.Decoder.Result
import io.circe.{Codec, DecodingFailure, HCursor, Json}

sealed trait CallbackData

object CallbackData:
case class DeleteProduct(index: Int) extends CallbackData

// TODO: Migrate to circe-generic-extras instead of manual codec when it is available for Scala 3
implicit val codec: Codec[CallbackData] = new Codec[CallbackData]:
override def apply(hCursor: HCursor): Result[CallbackData] =
hCursor.downField("type").as[String].flatMap {
case "DeleteProduct" =>
hCursor.downField("index").as[Int].map(DeleteProduct.apply)
case unknown =>
Left(DecodingFailure(s"Unknown type $unknown", hCursor.history))
}

override def apply(callbackData: CallbackData): Json = callbackData match
case DeleteProduct(index) =>
Json.obj(("type", Json.fromString("DeleteProduct")), ("index", Json.fromInt(index)))
1 change: 1 addition & 0 deletions src/main/scala/bot/OzonPriceCheckerBotCommands.scala
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,6 @@ object OzonPriceCheckerBotCommands:
val Start = "/start"
val Stop = "/stop"
val WatchNewProduct = "/watchnewproduct"
val UnwatchProduct = "/unwatchproduct"
val UnwatchAllProducts = "/unwatchallproducts"
val ShowAllProducts = "/showallproducts"
48 changes: 47 additions & 1 deletion src/main/scala/consumer/OzonPriceCheckerCommandProcessor.scala
Original file line number Diff line number Diff line change
@@ -1,25 +1,32 @@
package ru.ekuzmichev
package consumer

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

import io.circe.syntax.*
import org.telegram.telegrambots.meta.api.methods.send.SendMessage
import org.telegram.telegrambots.meta.api.objects.replykeyboard.InlineKeyboardMarkup
import org.telegram.telegrambots.meta.api.objects.replykeyboard.buttons.{InlineKeyboardButton, InlineKeyboardRow}
import org.telegram.telegrambots.meta.generics.TelegramClient
import zio.{Task, ZIO}

class OzonPriceCheckerCommandProcessor(productStore: ProductStore, telegramClient: TelegramClient)
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)
else
Expand Down Expand Up @@ -83,6 +90,45 @@ class OzonPriceCheckerCommandProcessor(productStore: ProductStore, telegramClien
.zipRight(sendTextMessage(sourceId.chatId, "I have removed all watched products."))
.logged(s"process command ${OzonPriceCheckerBotCommands.UnwatchAllProducts}")

private def processUnwatchProductCommand(sourceId: SourceId): Task[Unit] =
productStore
.readSourceState(sourceId)
.flatMap {
case Some(sourceState) => replyWithInnerDeletionKeyboard(sourceId, sourceState.products)
case None => askToSendStartCommand(sourceId)
}
.logged(s"process command ${OzonPriceCheckerBotCommands.UnwatchProduct}")

private def replyWithInnerDeletionKeyboard(sourceId: SourceId, products: Seq[ProductStore.Product]): Task[Unit] =
val rows: Seq[InlineKeyboardRow] = products.zipWithIndex.map { case (product, index) =>
new InlineKeyboardRow(
InlineKeyboardButton
.builder()
.text(s"$index) ${product.id} | ${product.priceThreshold}")
.callbackData(DeleteProduct(index).asInstanceOf[CallbackData].asJson.noSpaces)
.build()
)
}

val inlineKeyboardMarkup: InlineKeyboardMarkup =
rows
.foldLeft(
InlineKeyboardMarkup
.builder()
) { case (curr, next) => curr.keyboardRow(next) }
.build()

val sendMessage: SendMessage = SendMessage
.builder()
.chatId(sourceId.chatId)
.text("Choose which product you want to unwatch:")
.replyMarkup(inlineKeyboardMarkup)
.build()

ZIO.attempt {
telegramClient.execute(sendMessage)
}.unit

private def processShowAllProductsCommand(sourceId: SourceId): Task[Unit] =
productStore
.readSourceState(sourceId)
Expand Down
12 changes: 8 additions & 4 deletions src/main/scala/consumer/OzonPriceCheckerUpdateConsumer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import store.ProductStore.ProductCandidate.*
import store.ProductStore.{Product, ProductCandidate, SourceId}
import util.telegram.MessageSendingUtils.sendTextMessage

import org.telegram.telegrambots.meta.api.objects.Update
import org.telegram.telegrambots.meta.api.objects.{CallbackQuery, Update}
import org.telegram.telegrambots.meta.api.objects.message.Message
import org.telegram.telegrambots.meta.generics.TelegramClient
import zio.{LogAnnotation, Runtime, Task, ZIO}
Expand All @@ -26,10 +26,11 @@ class OzonPriceCheckerUpdateConsumer(

private implicit val _telegramClient: TelegramClient = telegramClient

//noinspection SimplifyWhenInspection
override def consumeZio(update: Update): Task[Unit] =
ZIO
.when(update.hasMessage)(processMessage(update.getMessage))
.unit
if update.hasMessage then processMessage(update.getMessage)
else if update.hasCallbackQuery then processCallbackQuery(update.getCallbackQuery)
else ZIO.unit

private def processMessage(message: Message): Task[Unit] =
val chatId: ChatId = message.getChatId.toString
Expand Down Expand Up @@ -122,6 +123,9 @@ class OzonPriceCheckerUpdateConsumer(

}

private def processCallbackQuery(callbackQuery: CallbackQuery): Task[Unit] =
ZIO.log(s"Callback data: ${callbackQuery.getData}")

private def onPriceThreshold(sourceId: SourceId, productId: ProductId, priceThreshold: Int) =
sendTextMessage(
sourceId.chatId,
Expand Down
5 changes: 5 additions & 0 deletions src/main/scala/store/CacheProductStore.scala
Original file line number Diff line number Diff line change
Expand Up @@ -58,5 +58,10 @@ class CacheProductStore(decoratee: ProductStore, cacheStateRepository: CacheStat

override def removeProduct(sourceId: SourceId, productId: ProductId): Task[Boolean] =
decoratee.removeProduct(sourceId, productId) <* replaceStateInCache()

override def removeProduct(sourceId: SourceId, productIndex: Int): Task[Boolean] =
decoratee.removeProduct(sourceId, productIndex) <* replaceStateInCache()




3 changes: 3 additions & 0 deletions src/main/scala/store/InMemoryProductStore.scala
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ class InMemoryProductStore(productStateRef: Ref[ProductState]) extends ProductSt
override def removeProduct(sourceId: SourceId, productId: ProductId): Task[Boolean] =
doUpdate(sourceId, sourceState => sourceState.copy(products = sourceState.products.filter(_.id != productId)))

override def removeProduct(sourceId: SourceId, productIndex: Int): Task[Boolean] =
doUpdate(sourceId, sourceState => sourceState.copy(products = sourceState.products.zipWithIndex.filter(_._2 != productIndex).map(_._1)))

private def doUpdateProductCandidate(
sourceId: SourceId,
maybeProductCandidate: Option[ProductCandidate]
Expand Down
1 change: 1 addition & 0 deletions src/main/scala/store/ProductStore.scala
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ trait ProductStore:
def resetProductCandidate(sourceId: SourceId): Task[Boolean]
def addProduct(sourceId: SourceId, product: ProductStore.Product): Task[Boolean]
def removeProduct(sourceId: SourceId, productId: ProductId): Task[Boolean]
def removeProduct(sourceId: SourceId, productIndex: Int): Task[Boolean]

object ProductStore:
case class SourceId(userName: UserName, chatId: ChatId) extends NamedToString
Expand Down
15 changes: 14 additions & 1 deletion src/test/scala/store/InMemoryProductStoreTest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ object InMemoryProductStoreTest extends ZIOSpecDefault:
hasWhiteLampProduct <- productStore.checkHasProductId(bobSourceId, whiteLampProductId)
yield assertTrue(hasRedKettleProduct, !hasWhiteLampProduct)
},
test("removeProduct should remove product from product list of sourceId if sourceId exists") {
test("removeProduct by productId should remove product from product list of sourceId if sourceId exists") {
import Fixtures.{bobSourceId, pamelaSourceId, redKettleProductId}
for
productStore <- makePreInitializedProductStore()
Expand All @@ -84,6 +84,19 @@ object InMemoryProductStoreTest extends ZIOSpecDefault:
!readBobSourceState.get.products.exists(_.id == redKettleProductId),
!pamelaProductRemoved
)
},
test("removeProduct by productIndex should remove product from product list of sourceId if sourceId exists") {
import Fixtures.{bobSourceId, pamelaSourceId, redKettleProductId}
for
productStore <- makePreInitializedProductStore()
bobProductRemoved <- productStore.removeProduct(bobSourceId, 0)
readBobSourceState <- productStore.readSourceState(bobSourceId)
pamelaProductRemoved <- productStore.removeProduct(pamelaSourceId, 0)
yield assertTrue(
bobProductRemoved,
!readBobSourceState.get.products.exists(_.id == redKettleProductId),
!pamelaProductRemoved
)
}
)

Expand Down

0 comments on commit c2e8e88

Please sign in to comment.