From 44ca91f7e94eb7ec4c303c8ec7ab3e49773598fc Mon Sep 17 00:00:00 2001
From: Pavel Kachalouski
Date: Sun, 30 Jul 2023 21:01:22 +0200
Subject: [PATCH] Cats experiments
---
.scalafmt.conf | 3 +
build.sbt | 25 +-
project/Dependencies.scala | 30 +-
project/build.properties | 2 +-
project/plugins.sbt | 5 +-
src/main/resources/logback.xml | 11 +-
src/main/scala/eu/xeppaka/bot/BotUri.scala | 15 -
.../eu/xeppaka/bot/CheckDeliveryDialog.scala | 302 ------------------
.../xeppaka/bot/CzechPostDeliveryCheck.scala | 248 --------------
.../scala/eu/xeppaka/bot/DialogManager.scala | 54 ----
src/main/scala/eu/xeppaka/bot/Main.scala | 49 ---
.../scala/eu/xeppaka/bot/TelegramBot.scala | 252 ---------------
src/main/scala/tech/xeppaka/bot/BotUri.scala | 16 +
.../xeppaka/bot/CheckDeliveryDialog.scala | 302 ++++++++++++++++++
.../xeppaka/bot/CzechPostDeliveryCheck.scala | 248 ++++++++++++++
.../tech/xeppaka/bot/DialogManager.scala | 54 ++++
.../xeppaka/bot/JsonSerializable.scala | 0
src/main/scala/tech/xeppaka/bot/Main.scala | 50 +++
.../{eu => tech}/xeppaka/bot/PostType.scala | 0
.../scala/tech/xeppaka/bot/TelegramBot.scala | 252 +++++++++++++++
.../tech/xeppaka/bot/cats/Delivery.scala | 5 +
.../scala/tech/xeppaka/bot/cats/Dialog.scala | 12 +
.../xeppaka/bot/cats/DialogDelivery.scala | 36 +++
.../tech/xeppaka/bot/cats/DialogStep.scala | 2 +
.../scala/tech/xeppaka/bot/cats/Errors.scala | 7 +
.../tech/xeppaka/bot/cats/IdDelivery.scala | 5 +
.../tech/xeppaka/bot/cats/TelegramBot.scala | 14 +
.../scala/tech/xeppaka/bot/cats/User.scala | 3 +
.../tech/xeppaka/bot/cats/Validation.scala | 11 +
29 files changed, 1051 insertions(+), 962 deletions(-)
create mode 100644 .scalafmt.conf
delete mode 100644 src/main/scala/eu/xeppaka/bot/BotUri.scala
delete mode 100644 src/main/scala/eu/xeppaka/bot/CheckDeliveryDialog.scala
delete mode 100644 src/main/scala/eu/xeppaka/bot/CzechPostDeliveryCheck.scala
delete mode 100644 src/main/scala/eu/xeppaka/bot/DialogManager.scala
delete mode 100644 src/main/scala/eu/xeppaka/bot/Main.scala
delete mode 100644 src/main/scala/eu/xeppaka/bot/TelegramBot.scala
create mode 100644 src/main/scala/tech/xeppaka/bot/BotUri.scala
create mode 100644 src/main/scala/tech/xeppaka/bot/CheckDeliveryDialog.scala
create mode 100644 src/main/scala/tech/xeppaka/bot/CzechPostDeliveryCheck.scala
create mode 100644 src/main/scala/tech/xeppaka/bot/DialogManager.scala
rename src/main/scala/{eu => tech}/xeppaka/bot/JsonSerializable.scala (100%)
create mode 100644 src/main/scala/tech/xeppaka/bot/Main.scala
rename src/main/scala/{eu => tech}/xeppaka/bot/PostType.scala (100%)
create mode 100644 src/main/scala/tech/xeppaka/bot/TelegramBot.scala
create mode 100644 src/main/scala/tech/xeppaka/bot/cats/Delivery.scala
create mode 100644 src/main/scala/tech/xeppaka/bot/cats/Dialog.scala
create mode 100644 src/main/scala/tech/xeppaka/bot/cats/DialogDelivery.scala
create mode 100644 src/main/scala/tech/xeppaka/bot/cats/DialogStep.scala
create mode 100644 src/main/scala/tech/xeppaka/bot/cats/Errors.scala
create mode 100644 src/main/scala/tech/xeppaka/bot/cats/IdDelivery.scala
create mode 100644 src/main/scala/tech/xeppaka/bot/cats/TelegramBot.scala
create mode 100644 src/main/scala/tech/xeppaka/bot/cats/User.scala
create mode 100644 src/main/scala/tech/xeppaka/bot/cats/Validation.scala
diff --git a/.scalafmt.conf b/.scalafmt.conf
new file mode 100644
index 0000000..ab69e7d
--- /dev/null
+++ b/.scalafmt.conf
@@ -0,0 +1,3 @@
+version = 3.7.11
+maxColumn = 140
+runner.dialect = scala3
diff --git a/build.sbt b/build.sbt
index 6b6ce7c..36148a4 100644
--- a/build.sbt
+++ b/build.sbt
@@ -1,7 +1,7 @@
import Dependencies._
import Versions._
-lazy val commonSettings = Seq(organization := "eu.xeppaka", scalaVersion := "2.13.4", mainClass := Some("eu.xeppaka.bot.Main"))
+lazy val commonSettings = Seq(organization := "eu.xeppaka", scalaVersion := "3.3.0", mainClass := Some("tech.xeppaka.bot.Main"))
inThisBuild(commonSettings)
@@ -9,21 +9,14 @@ lazy val `telegram-bot-delivery` = (project in file("."))
.settings(
name := "telegram-bot-delivery",
libraryDependencies ++= Seq(
- akkaTyped,
- akkaSerializationJackson,
- akkaClusterShardingTyped,
- akkaHttp,
- akkaHttpJackson,
- akkaStream,
- akkaPersistence,
- akkaPersistenceCassandra,
- akkaPersistenceQuery,
- akkaTestkitTyped % Test,
- scalaTest % Test,
- slibTelegram,
- logback
- ),
- dependencyOverrides ++= Seq("com.typesafe.akka" %% "akka-http-jackson" % akkaHttpVersion),
+ catsCore,
+ catsEffect,
+ sttpClient,
+ tapirHttp4sServer,
+ scalaTest % Test,
+ slibTelegram,
+ logback
+ ),
dockerBaseImage := "openjdk:11",
dockerExposedPorts := Seq(8443),
dockerRepository := Some("registry.xeppaka.eu:443"),
diff --git a/project/Dependencies.scala b/project/Dependencies.scala
index 8932854..2e8890f 100644
--- a/project/Dependencies.scala
+++ b/project/Dependencies.scala
@@ -4,26 +4,20 @@ import Dependencies.Versions._
object Dependencies {
object Versions {
- val akkaVersion = "2.6.10"
- val akkaHttpVersion = "10.2.2"
- val akkaHttpJacksonVersion = "1.35.2"
- val akkaPersistenceCassandraVersion = "1.0.4"
- val scalaTestVersion = "3.2.2"
+ val catsEffectVersion = "3.5.0"
+ val catsVersion = "2.9.0"
+ val sttpClientVersion = "3.8.16"
+ val logbackVersion = "1.4.8"
+ val scalaTestVersion = "3.2.16"
val slibTelegramVersion = "0.1.0"
- val logbackVersion = "1.2.3"
+ val tapirVersion = "1.6.4"
}
- val akkaTyped = "com.typesafe.akka" %% "akka-actor-typed" % akkaVersion
- val akkaStream = "com.typesafe.akka" %% "akka-stream" % akkaVersion
- val akkaSerializationJackson = "com.typesafe.akka" %% "akka-serialization-jackson" % akkaVersion
- val akkaHttp = "com.typesafe.akka" %% "akka-http" % akkaHttpVersion
- val akkaHttpJackson = "de.heikoseeberger" %% "akka-http-jackson" % akkaHttpJacksonVersion
- val akkaPersistence = "com.typesafe.akka" %% "akka-persistence-typed" % akkaVersion
- val akkaClusterShardingTyped = "com.typesafe.akka" %% "akka-cluster-sharding-typed" % akkaVersion
- val akkaPersistenceCassandra = "com.typesafe.akka" %% "akka-persistence-cassandra" % akkaPersistenceCassandraVersion
- val akkaPersistenceQuery = "com.typesafe.akka" %% "akka-persistence-query" % akkaVersion
- val akkaTestkitTyped = "com.typesafe.akka" %% "akka-actor-testkit-typed" % akkaVersion
- val slibTelegram = "eu.xeppaka" %% "slib-telegram" % slibTelegramVersion
- val scalaTest = "org.scalatest" %% "scalatest" % scalaTestVersion
+ val catsCore = "org.typelevel" %% "cats-core" % catsVersion
+ val catsEffect = "org.typelevel" %% "cats-effect" % catsEffectVersion
+ val sttpClient = "com.softwaremill.sttp.client3" %% "core" % sttpClientVersion
val logback = "ch.qos.logback" % "logback-classic" % logbackVersion
+ val scalaTest = "org.scalatest" %% "scalatest" % scalaTestVersion
+ val slibTelegram = "tech.xeppaka" %% "slib-telegram" % slibTelegramVersion
+ val tapirHttp4sServer = "com.softwaremill.sttp.tapir" %% "tapir-http4s-server" % tapirVersion
}
diff --git a/project/build.properties b/project/build.properties
index c06db1b..52413ab 100644
--- a/project/build.properties
+++ b/project/build.properties
@@ -1 +1 @@
-sbt.version=1.4.5
+sbt.version=1.9.3
diff --git a/project/plugins.sbt b/project/plugins.sbt
index 4368552..9971882 100644
--- a/project/plugins.sbt
+++ b/project/plugins.sbt
@@ -1,2 +1,3 @@
-addSbtPlugin("com.typesafe.sbt" % "sbt-native-packager" % "1.8.0")
-addSbtPlugin("com.github.gseitz" % "sbt-release" % "1.0.13")
+addSbtPlugin("com.github.sbt" % "sbt-native-packager" % "1.9.16")
+addSbtPlugin("com.github.sbt" % "sbt-release" % "1.1.0")
+addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.4.6")
diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml
index 7cd947a..96479ef 100644
--- a/src/main/resources/logback.xml
+++ b/src/main/resources/logback.xml
@@ -1,11 +1,12 @@
-
+
+
+
- %date [%level] %logger: %message%n%xException
+ %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
-
-
+
-
+
\ No newline at end of file
diff --git a/src/main/scala/eu/xeppaka/bot/BotUri.scala b/src/main/scala/eu/xeppaka/bot/BotUri.scala
deleted file mode 100644
index 100fddd..0000000
--- a/src/main/scala/eu/xeppaka/bot/BotUri.scala
+++ /dev/null
@@ -1,15 +0,0 @@
-package eu.xeppaka.bot
-
-import akka.http.scaladsl.model.Uri
-
-case class BotUri(botId: String) {
- private val baseUri = Uri(s"https://api.telegram.org/bot$botId")
-
- val botUri: Uri = baseUri
- val getMe: Uri = baseUri.withPath(baseUri.path / "getMe")
- val setWebhook: Uri = baseUri.withPath(baseUri.path / "setWebhook")
- val deleteWebhook: Uri = baseUri.withPath(baseUri.path / "deleteWebhook")
- val getWebhookInfo: Uri = baseUri.withPath(baseUri.path / "getWebhookInfo")
- val sendMessage: Uri = baseUri.withPath(baseUri.path / "sendMessage")
- val editMessageReplyMarkup: Uri = baseUri.withPath(baseUri.path / "editMessageReplyMarkup")
-}
diff --git a/src/main/scala/eu/xeppaka/bot/CheckDeliveryDialog.scala b/src/main/scala/eu/xeppaka/bot/CheckDeliveryDialog.scala
deleted file mode 100644
index f11b3be..0000000
--- a/src/main/scala/eu/xeppaka/bot/CheckDeliveryDialog.scala
+++ /dev/null
@@ -1,302 +0,0 @@
-package eu.xeppaka.bot
-
-import akka.actor.ActorSystem
-import akka.actor.typed.scaladsl.Behaviors
-import akka.actor.typed.scaladsl.adapter._
-import akka.http.scaladsl.marshalling.Marshal
-import akka.actor.typed.{ ActorRef, Behavior, SupervisorStrategy }
-import akka.http.scaladsl.Http
-import akka.http.scaladsl.model._
-import akka.stream.scaladsl.{ Sink, Source }
-import akka.util.{ ByteString, Timeout }
-import eu.xeppaka.telegram.bot.TelegramEntities._
-
-import scala.concurrent.{ Await, ExecutionContext }
-import scala.concurrent.duration._
-import scala.util.{ Failure, Success }
-
-object CheckDeliveryDialog {
- import de.heikoseeberger.akkahttpjackson.JacksonSupport._
-
- sealed trait Command
- sealed trait CommandResult
- sealed trait DialogCommand extends Command
-
- case class ProcessMessage(msg: Message, replyTo: ActorRef[CommandResult]) extends Command
- case object ProcessMessageSuccess extends CommandResult
- case class ProcessMessageFailure(exception: Throwable) extends CommandResult
-
- case object AddParcel extends DialogCommand
- case object RemoveParcel extends DialogCommand
- case object ListParcels extends DialogCommand
- case object Help extends DialogCommand
-
- object DialogCommand {
- def parse(text: String): DialogCommand = text match {
- case "/add" => AddParcel
- case "/remove" => RemoveParcel
- case "/list" => ListParcels
- case "/help" => Help
- case _ => Help
- }
- }
-
- // internal messages
- private case class DeliveryStateChanged(state: String) extends Command
- private val helpMessage =
- """
- |Supported commands:
- |/add - add parcel to a list of watched parcels
- |/list - list watched parcels
- |/remove - remove parcel from a watching list
- """.stripMargin
- private val commandsKeyboard = Some(
- ReplyKeyboardMarkup(Seq(Seq(KeyboardButton("/add"), KeyboardButton("/list"), KeyboardButton("/remove"))), resize_keyboard = Some(true), one_time_keyboard = Some(true))
- )
-
- private val removeKeyboard = Some(ReplyKeyboardRemove())
-
- def behavior(chatId: Long, botUri: BotUri): Behavior[Command] = Behaviors.setup[Command] { ctx =>
- implicit val system: ActorSystem = ctx.system.toClassic
- implicit val executionContext: ExecutionContext = ctx.executionContext
- val http = Http()
- val deliveryStateAdapter: ActorRef[CzechPostDeliveryCheck.DeliveryStateChanged] = ctx.messageAdapter(stateChanged => DeliveryStateChanged(stateChanged.state))
- val czechPostDeliveryCheck = ctx.spawnAnonymous(Behaviors.supervise(CzechPostDeliveryCheck.behavior(chatId.toString, deliveryStateAdapter)).onFailure(SupervisorStrategy.restart))
-
- def waitCommand: Behavior[Command] = Behaviors.receiveMessage {
- case ProcessMessage(msg, replyTo) =>
- val command = msg.text.map(text => DialogCommand.parse(text))
- replyTo ! ProcessMessageSuccess
-
- if (command.isDefined) {
- ctx.self ! command.get
- Behaviors.same
- } else {
- val message = SendMessage(chatId, "This command is unsupported.")
- sendMessage(message, waitCommand, waitCommand)
- }
- case AddParcel =>
- val parcelIdMessage = SendMessage(chatId, "Please enter a parcel ID.", reply_markup = removeKeyboard)
- val commentMessage = SendMessage(chatId, "Please enter a comment.", reply_markup = removeKeyboard)
- sendMessage(parcelIdMessage, waitTextMessage(parcelId => sendMessage(commentMessage, waitTextMessage(comment => addParcel(parcelId, comment)), waitCommand)), waitCommand)
- case RemoveParcel =>
- removeParcel(waitCommand, waitCommand)
- case ListParcels =>
- listParcels
- case Help =>
- val message = SendMessage(chatId, helpMessage, reply_markup = commandsKeyboard)
- sendMessage(message, waitCommand, waitCommand)
- case DeliveryStateChanged(state) =>
- val message = SendMessage(chatId, state, Some("Markdown"))
- sendMessage(message, waitCommand, waitCommand)
- case _ =>
- Behaviors.unhandled
- }
-
- def addParcel(parcelId: String, comment: String): Behavior[Command] = Behaviors.withStash(100) { stashBuffer =>
- Behaviors.setup { ctx =>
- case object AddParcelSuccess extends Command
- case class AddParcelFailure(exception: Throwable) extends Command
- implicit val timeout: Timeout = 5.seconds
-
- ctx.ask[CzechPostDeliveryCheck.Command, CzechPostDeliveryCheck.CommandResult](czechPostDeliveryCheck, ref => CzechPostDeliveryCheck.AddParcel(parcelId, comment, ref)) {
- case Success(CzechPostDeliveryCheck.CommandResultSuccess) => AddParcelSuccess
- case Success(CzechPostDeliveryCheck.CommandResultFailure(exception)) => AddParcelFailure(exception)
- case Failure(exception) => AddParcelFailure(exception)
- }
-
- Behaviors.receiveMessage {
- case AddParcelSuccess =>
- val message = SendMessage(chatId, s"Parcel $parcelId was added to the watch list.", reply_markup = commandsKeyboard)
- sendMessage(message, waitCommand, waitCommand)
- case AddParcelFailure(exception) =>
- exception match {
- case CzechPostDeliveryCheck.DuplicateParcelId(_) =>
- val message = SendMessage(chatId, s"Parcel $parcelId is in the watch list already.", reply_markup = commandsKeyboard)
- sendMessage(message, waitCommand, waitCommand)
- case _ =>
- ctx.log.error("action=add_parcel result=failure", exception)
- val message = SendMessage(chatId, s"Adding parcel failed. Please try again.", reply_markup = commandsKeyboard)
- sendMessage(message, waitCommand, waitCommand)
- }
- case otherMessage =>
- stashBuffer.stash(otherMessage)
- Behaviors.same
- }
- }
- }
-
- def listParcels: Behavior[Command] = Behaviors.withStash(100) { stashBuffer =>
- Behaviors.setup { ctx =>
- case class ListParcelsSuccess(parcelsList: Seq[String]) extends Command
- case class ListParcelsFailure(exception: Throwable) extends Command
- implicit val timeout: Timeout = 5.seconds
-
- ctx.ask[CzechPostDeliveryCheck.Command, CzechPostDeliveryCheck.ListParcelsResult](czechPostDeliveryCheck, ref => CzechPostDeliveryCheck.ListParcels(ref)) {
- case Success(CzechPostDeliveryCheck.ListParcelsResult(parcelsList)) => ListParcelsSuccess(parcelsList)
- case Failure(exception) => ListParcelsFailure(exception)
- }
-
- Behaviors.receiveMessage {
- case ListParcelsSuccess(parcelsList) =>
- val messageText = "*List of your watched parcels:*\n" + (if (parcelsList.nonEmpty) parcelsList.sorted.mkString("\n") else "(empty)")
- val message = SendMessage(chatId, messageText, Some("Markdown"), reply_markup = commandsKeyboard)
- sendMessage(message, waitCommand, waitCommand)
- case ListParcelsFailure(exception) =>
- ctx.log.error(s"action=list_parcels result=failure chat_id=$chatId", exception)
- val message = SendMessage(chatId, "Failed to get a list of your watched parcels. Please try again later.", reply_markup = commandsKeyboard)
- sendMessage(message, waitCommand, waitCommand)
- case otherMessage =>
- stashBuffer.stash(otherMessage)
- Behaviors.same
- }
- }
- }
-
- def removeParcel(onSuccess: => Behavior[Command], onFailure: => Behavior[Command]): Behavior[Command] = Behaviors.withStash(100) { stashBuffer =>
- Behaviors.setup { ctx =>
- case class ListParcelIdsSuccess(parcelsList: Seq[String]) extends Command
- case class ListParcelIdsFailure(exception: Throwable) extends Command
- implicit val timeout: Timeout = 5.seconds
-
- ctx.ask[CzechPostDeliveryCheck.Command, CzechPostDeliveryCheck.ListParcelIdsResult](czechPostDeliveryCheck, ref => CzechPostDeliveryCheck.ListParcelIds(ref)) {
- case Success(CzechPostDeliveryCheck.ListParcelIdsResult(parcelsList)) => ListParcelIdsSuccess(parcelsList)
- case Failure(exception) => ListParcelIdsFailure(exception)
- }
-
- Behaviors.receiveMessage {
- case ListParcelIdsSuccess(parcelsList) =>
- if (parcelsList.nonEmpty) {
- val keyboardButtons = parcelsList.sorted.grouped(3).map(_.map(id => KeyboardButton(id))).toSeq
- val markup = ReplyKeyboardMarkup(keyboard = keyboardButtons, resize_keyboard = Some(true), one_time_keyboard = Some(true))
- val message = SendMessage(chatId, "Please enter a parcel id to remove.", reply_markup = Some(markup))
- sendMessage(message, waitTextMessage(parcelId => removeParcelId(parcelId)), onFailure)
- } else {
- val message = SendMessage(chatId, "You don't have watched parcels. There is nothing to remove.", reply_markup = commandsKeyboard)
- sendMessage(message, onSuccess, onFailure)
- }
- case ListParcelIdsFailure(exception) =>
- ctx.log.error(s"action=list_parcels result=failure chat_id=$chatId", exception)
- val message = SendMessage(chatId, "Failed to get a list of your watched parcels. Please try again later.", reply_markup = commandsKeyboard)
- sendMessage(message, waitCommand, waitCommand)
- case otherMessage =>
- stashBuffer.stash(otherMessage)
- Behaviors.same
- }
- }
- }
-
- def removeParcelId(parcelId: String): Behavior[Command] = Behaviors.withStash(100) { stashBuffer =>
- Behaviors.setup { ctx =>
- case object RemoveParcelSuccess extends Command
- case class RemoveParcelFailure(exception: Throwable) extends Command
- implicit val timeout: Timeout = 5.seconds
-
- ctx.ask[CzechPostDeliveryCheck.Command, CzechPostDeliveryCheck.CommandResult](czechPostDeliveryCheck, ref => CzechPostDeliveryCheck.RemoveParcel(parcelId, ref)) {
- case Success(CzechPostDeliveryCheck.CommandResultSuccess) => RemoveParcelSuccess
- case Success(CzechPostDeliveryCheck.CommandResultFailure(exception)) => RemoveParcelFailure(exception)
- case Failure(exception) => RemoveParcelFailure(exception)
- }
-
- Behaviors.receiveMessage {
- case RemoveParcelSuccess =>
- val message = SendMessage(chatId, s"Parcel $parcelId was removed from the watch list.", reply_markup = commandsKeyboard)
- sendMessage(message, waitCommand, waitCommand)
- case RemoveParcelFailure(exception) =>
- exception match {
- case CzechPostDeliveryCheck.ParcelIdNotFound(_) =>
- val message = SendMessage(chatId, s"Parcel $parcelId is not found in the list of the watched parcels.", reply_markup = commandsKeyboard)
- sendMessage(message, waitCommand, waitCommand)
- case _ =>
- ctx.log.error("action=add_parcel result=failure", exception)
- val message = SendMessage(chatId, s"Remove of the parcel failed. Please try again.", reply_markup = commandsKeyboard)
- sendMessage(message, waitCommand, waitCommand)
- }
- case otherMessage =>
- stashBuffer.stash(otherMessage)
- Behaviors.same
- }
- }
- }
-
- // def selectPostType(onFinish: PostType => Behavior[Command]): Behavior[Command] = Behaviors.receiveMessage {
- //
- // case ProcessMessage(msg, replyTo) =>
- // val button1 = KeyboardButton("button1")
- // val button2 = KeyboardButton("button2")
- // val keyboard = ReplyKeyboardMarkup(Seq(Seq(button1, button2)))
- // val message = SendMessage(chatId, "Please enter parcel ID.", reply_markup = Some(keyboard))
- // sendMessage(message, waitParcelId(parcelId => addParcel(parcelId)), waitCommand)
- // }
-
- def waitTextMessage(onFinish: String => Behavior[Command]): Behavior[Command] = Behaviors.withStash(100) { stashBuffer =>
- Behaviors.receiveMessage {
- case ProcessMessage(msg, replyTo) =>
- if (msg.text.isDefined) {
- val parcelId = msg.text.get
- replyTo ! ProcessMessageSuccess
- onFinish(parcelId)
- } else {
- replyTo ! ProcessMessageSuccess
- waitTextMessage(onFinish)
- }
- case otherMsg =>
- stashBuffer.stash(otherMsg)
- Behaviors.same
- }
- }
-
- def sendMessage(message: SendMessage, onSuccess: => Behavior[Command], onFailure: => Behavior[Command], attempt: Int = 1): Behavior[Command] = Behaviors.withStash(100) { stashBuffer =>
- Behaviors.setup[Command] { ctx =>
-
- case object SendMessageSuccess extends Command
- case class SendMessageFailure(exception: Throwable) extends Command
-
- ctx.log.debug("action=send_message status=started chat_id={} message={}", chatId, message)
-
- println(message)
- println(Await.result(Marshal(message).to[HttpEntity], 2.seconds).asInstanceOf[HttpEntity.Strict].data.utf8String)
-
- Source
- .future(Marshal(message).to[RequestEntity])
- .initialDelay(2.seconds * (attempt - 1))
- .map(requestEntity => HttpRequest(HttpMethods.POST, uri = botUri.sendMessage, entity = requestEntity))
- .mapAsync(1) { request =>
- http.singleRequest(request).transform {
- case Success(response) =>
- if (response.status.isSuccess()) {
- Success(SendMessageSuccess)
- } else {
- Success(SendMessageFailure(new RuntimeException(s"Error while sending message. HTTP status: ${response.status}.")))
- }
- case Failure(exception) =>
- ctx.log.error(s"action=send_message status=finished result=failure chat_id=$chatId", exception)
- Success(SendMessageFailure(exception))
- }
- }
- .to(Sink.foreach(ctx.self ! _))
- .run()
-
- Behaviors.receiveMessage {
- case SendMessageSuccess =>
- ctx.log.debug("action=send_message status=finished result=success chat_id={}", chatId)
- stashBuffer.unstashAll(onSuccess)
- case SendMessageFailure(exception) =>
- ctx.log.error(s"action=send_message status=finished result=failure chat_id=$chatId attempt=$attempt", exception)
-
- if (attempt > 5) {
- ctx.log.error("action=send_message result=failure message=attempts threshold exceeded", exception)
- stashBuffer.unstashAll(onFailure)
- } else {
- sendMessage(message, onSuccess, onFailure, attempt + 1)
- }
- case otherMsg =>
- stashBuffer.stash(otherMsg)
- Behaviors.same
- }
- }
- }
-
- waitCommand
- }
-}
diff --git a/src/main/scala/eu/xeppaka/bot/CzechPostDeliveryCheck.scala b/src/main/scala/eu/xeppaka/bot/CzechPostDeliveryCheck.scala
deleted file mode 100644
index 935068d..0000000
--- a/src/main/scala/eu/xeppaka/bot/CzechPostDeliveryCheck.scala
+++ /dev/null
@@ -1,248 +0,0 @@
-package eu.xeppaka.bot
-
-import akka.actor.ActorSystem
-import akka.actor.typed.scaladsl.Behaviors
-import akka.actor.typed.scaladsl.adapter._
-import akka.actor.typed.{ ActorRef, Behavior, DispatcherSelector }
-import akka.http.scaladsl.model._
-import akka.http.scaladsl.model.headers.{ `User-Agent`, Accept }
-import akka.http.scaladsl.settings.{ ClientConnectionSettings, ConnectionPoolSettings }
-import akka.http.scaladsl.unmarshalling.Unmarshal
-import akka.http.scaladsl.{ ConnectionContext, Http }
-import akka.persistence.typed.PersistenceId
-import akka.persistence.typed.scaladsl.EventSourcedBehavior.{ CommandHandler, EventHandler }
-import akka.persistence.typed.scaladsl.{ Effect, EventSourcedBehavior }
-import com.fasterxml.jackson.annotation.{ JsonSubTypes, JsonTypeInfo }
-import com.typesafe.sslconfig.akka.AkkaSSLConfig
-
-import java.security.cert.X509Certificate
-import java.text.SimpleDateFormat
-import javax.net.ssl.{ KeyManager, SSLContext, X509TrustManager }
-import scala.collection.immutable
-import scala.concurrent.ExecutionContextExecutor
-import scala.concurrent.duration._
-import scala.util.{ Failure, Success }
-
-object Entities {
-
- case class Attributes(parcelType: String, weight: Double, currency: String)
-
- case class State(
- id: String,
- date: String,
- text: String,
- postcode: Option[String],
- postoffice: Option[String],
- idIcon: Option[Int],
- publicAccess: Int,
- latitude: Option[Double],
- longitude: Option[Double],
- timeDeliveryAttempt: Option[String]
- )
-
- case class States(state: Seq[State])
-
- case class ParcelHistory(id: String, attributes: Attributes, states: States)
-}
-
-object CzechPostDeliveryCheck {
- import de.heikoseeberger.akkahttpjackson.JacksonSupport._
-
- private val czechPostDateFormat = new SimpleDateFormat("yyyy-MM-dd")
- private val printDateFormat = new SimpleDateFormat("dd-MM-yyyy")
- private val entityType = "czechpost"
-
- sealed trait Command extends JsonSerializable
- sealed trait CommandResult extends JsonSerializable
- @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type")
- @JsonSubTypes(
- Array(
- new JsonSubTypes.Type(value = classOf[ParcelAttributesChanged], name = "parcel_attributes_changed"),
- new JsonSubTypes.Type(value = classOf[ParcelAdded], name = "parcel_added"),
- new JsonSubTypes.Type(value = classOf[ParcelRemoved], name = "parcel_removed"),
- new JsonSubTypes.Type(value = classOf[ParcelHistoryStateAdded], name = "parcel_history_state_added")
- )
- )
- sealed trait Event extends JsonSerializable
- case class Parcel(comment: String, attributes: Option[Entities.Attributes] = None, states: Set[Entities.State] = Set.empty) {
- def fullStatePrint(parcelId: String): String = {
- val statesString = states.toSeq
- .sortBy(state => czechPostDateFormat.parse(state.date))
- .map(state => s"${printDateFormat.format(czechPostDateFormat.parse(state.date))} - ${state.text}\n===========================\n")
- .mkString
-
- s"""|*New state(s) of the parcel $parcelId ($comment):*
- |===========================
- |$statesString""".stripMargin
- }
-
- def latestStatePrint(parcelId: String): String = {
- latestState.map(state => s"$parcelId ($comment) - ${printDateFormat.format(czechPostDateFormat.parse(state.date))} - ${state.text}").getOrElse(s"$parcelId ($comment) - NO INFO")
- }
-
- private def latestState: Option[Entities.State] = states.toSeq.maxByOption(state => czechPostDateFormat.parse(state.date))
- }
-
- case class State(parcelStates: Map[String, Parcel] = Map.empty) extends JsonSerializable {
- def latestStatesPrint: Seq[String] = parcelStates.map { case (id, parcel) => parcel.latestStatePrint(id) }.to(Seq)
- }
-
- case class AddParcel(parcelId: String, comment: String, replyTo: ActorRef[CommandResult]) extends Command
- case class RemoveParcel(parcelId: String, replyTo: ActorRef[CommandResult]) extends Command
- case class ListParcels(replyTo: ActorRef[ListParcelsResult]) extends Command
- case class ListParcelsResult(parcelsList: Seq[String])
- case class ListParcelIds(replyTo: ActorRef[ListParcelIdsResult]) extends Command
- case class ListParcelIdsResult(parcelIds: Seq[String])
-
- case object CommandResultSuccess extends CommandResult
- case class CommandResultFailure(exception: Throwable) extends CommandResult
-
- case class ParcelIdNotFound(parcelId: String) extends Exception
- case class DuplicateParcelId(parcelId: String) extends Exception
-
- // internal commands
- private case object CheckParcels extends Command
- private case class ParcelHistoryRetrieved(parcelHistory: Entities.ParcelHistory) extends Command
- case class DeliveryStateChanged(state: String)
-
- case class ParcelAdded(parcelId: String, comment: String) extends Event
- case class ParcelRemoved(parcelId: String) extends Event
- case class ParcelHistoryStateAdded(parcelId: String, state: Entities.State) extends Event
- case class ParcelAttributesChanged(parcelId: String, attributes: Entities.Attributes) extends Event
-
- private val trustfulSslContext: SSLContext = {
- object NoCheckX509TrustManager extends X509TrustManager {
- override def checkClientTrusted(chain: Array[X509Certificate], authType: String): Unit = ()
- override def checkServerTrusted(chain: Array[X509Certificate], authType: String): Unit = ()
- override def getAcceptedIssuers: Array[X509Certificate] = Array[X509Certificate]()
- }
-
- val context = SSLContext.getInstance("TLS")
- context.init(Array[KeyManager](), Array(NoCheckX509TrustManager), null)
- context
- }
-
- def behavior(chatId: String, stateReporter: ActorRef[DeliveryStateChanged]): Behavior[Command] = checkParcel(chatId, stateReporter)
-
- private def checkParcel(chatId: String, stateReporter: ActorRef[DeliveryStateChanged]): Behavior[Command] = Behaviors.withTimers { scheduler =>
- Behaviors.setup { ctx =>
- implicit val actorSystem: ActorSystem = ctx.system.toClassic
- implicit val executionContext: ExecutionContextExecutor = ctx.system.dispatchers.lookup(DispatcherSelector.default())
- val http = Http()
- val badSslConfig = AkkaSSLConfig().mapSettings(s => s.withLoose(s.loose.withAcceptAnyCertificate(true).withDisableHostnameVerification(true)))
- val originalCtx = http.createClientHttpsContext(badSslConfig)
- val sslContext = ConnectionContext.httpsClient(trustfulSslContext)
- val clientConnectionSettings = ClientConnectionSettings(actorSystem).withUserAgentHeader(Some(`User-Agent`("Mozilla/5.0 (X11; Linux x86_64; rv:62.0) Gecko/20100101 Firefox/62.0")))
- val connectionSettings = ConnectionPoolSettings(actorSystem).withConnectionSettings(clientConnectionSettings)
-
- scheduler.startTimerAtFixedRate("check-delivery-state", CheckParcels, 5.minutes)
-
- val log = ctx.log
-
- val commandHandler: CommandHandler[Command, Event, State] = (state, cmd) => {
- cmd match {
- case AddParcel(parcelId, comment, replyTo) =>
- val parcelIdUpper = parcelId.toUpperCase
- if (state.parcelStates.keySet.contains(parcelIdUpper)) {
- Effect.none.thenRun(_ => replyTo ! CommandResultFailure(DuplicateParcelId(parcelIdUpper)))
- } else {
- Effect
- .persist(ParcelAdded(parcelIdUpper, comment))
- .thenRun(_ => {
- replyTo ! CommandResultSuccess
- ctx.self ! CheckParcels
- })
- }
- case RemoveParcel(parcelId, replyTo) =>
- val parcelIdUpper = parcelId.toUpperCase
- if (state.parcelStates.contains(parcelIdUpper)) {
- Effect.persist(ParcelRemoved(parcelIdUpper)).thenRun(_ => replyTo ! CommandResultSuccess)
- } else {
- Effect.none.thenRun(_ => replyTo ! CommandResultFailure(ParcelIdNotFound(parcelIdUpper)))
- }
-
- case ListParcels(replyTo) =>
- Effect.none.thenRun { state =>
- val parcelsList = state.latestStatesPrint
- replyTo ! ListParcelsResult(parcelsList)
- }
-
- case ListParcelIds(replyTo) =>
- Effect.none.thenRun { state =>
- replyTo ! ListParcelIdsResult(state.parcelStates.keys.toSeq)
- }
-
- case CheckParcels =>
- Effect.none.thenRun { _ =>
- log.info("action=check_parcel_state chat_id={}", chatId)
- val parcelIds = state.parcelStates.keys.grouped(10).map(ids => ids.foldLeft("")((acc, id) => if (acc.isEmpty) id else s"$acc;$id"))
-
- for (ids <- parcelIds) {
- val checkUri = Uri(s"https://b2c.cpost.cz/services/ParcelHistory/getDataAsJson?idParcel=$ids&language=cz")
- val request = HttpRequest(uri = checkUri, headers = immutable.Seq(Accept(MediaTypes.`application/json`)))
-
- log.info("action=check_parcel_state chat_id={} check_uri={}", chatId, checkUri)
-
- http
- .singleRequest(request, connectionContext = sslContext, settings = connectionSettings)
- .transform {
- case Success(response) => if (response.status.isSuccess()) Success(response) else Failure(new Exception(s"Check parcel returned HTTP status: ${response.status.value}."))
- case response: Failure[HttpResponse] => response
- }
- .flatMap(response => Unmarshal(response).to[Seq[Entities.ParcelHistory]])
- .andThen {
- case Success(parcelHistories) =>
- parcelHistories.foreach(parcelHistory => ctx.self ! ParcelHistoryRetrieved(parcelHistory))
- case Failure(exception) =>
- log.error("Error checking parcel history.", exception)
- }
- .andThen {
- case Success(_) => log.info("action=check_parcel_state result=success chat_id={} check_uri={}", chatId, checkUri)
- case Failure(exception) => log.error(s"action=check_parcel_state result=failure chat_id=$chatId check_uri=$checkUri", exception)
- }
- }
- }
- case ParcelHistoryRetrieved(parcelHistory) =>
- val parcelId = parcelHistory.id
- val parcelState = state.parcelStates(parcelId)
- val attributesChangedEvents: Seq[Event] = (if (parcelState.attributes.isEmpty)
- Some(parcelHistory.attributes)
- else
- parcelState.attributes.flatMap(oldAttributes => if (oldAttributes != parcelHistory.attributes) Some(parcelHistory.attributes) else None))
- .map(attributes => ParcelAttributesChanged(parcelId, attributes))
- .toSeq
-
- val newStates = parcelHistory.states.state.toSet -- parcelState.states
- val stateEvents: Seq[Event] = newStates.map(state => ParcelHistoryStateAdded(parcelId, state)).toSeq
- val comment = state.parcelStates(parcelId).comment
-
- Effect
- .persist(attributesChangedEvents ++ stateEvents)
- .thenRun(_ => {
- if (newStates.nonEmpty) {
- stateReporter ! DeliveryStateChanged(Parcel(comment, None, newStates).fullStatePrint(parcelId))
- }
- })
- }
- }
-
- val eventHandler: EventHandler[State, Event] = (state, evt) => {
- evt match {
- case ParcelAdded(parcelId, comment) =>
- state.copy(parcelStates = state.parcelStates + (parcelId -> Parcel(comment)))
- case ParcelRemoved(parcelId) => state.copy(parcelStates = state.parcelStates - parcelId)
- case ParcelHistoryStateAdded(parcelId, newState) =>
- val parcelState = state.parcelStates(parcelId)
- val newParcelState = parcelState.copy(states = parcelState.states + newState)
- state.copy(parcelStates = state.parcelStates.updated(parcelId, newParcelState))
- case ParcelAttributesChanged(parcelId, newAttributes) =>
- val parcelState = state.parcelStates(parcelId)
- val newParcelState = parcelState.copy(attributes = Some(newAttributes))
- state.copy(parcelStates = state.parcelStates.updated(parcelId, newParcelState))
- }
- }
-
- EventSourcedBehavior[Command, Event, State](persistenceId = PersistenceId(entityType, chatId), emptyState = State(), commandHandler = commandHandler, eventHandler = eventHandler)
- }
- }
-}
diff --git a/src/main/scala/eu/xeppaka/bot/DialogManager.scala b/src/main/scala/eu/xeppaka/bot/DialogManager.scala
deleted file mode 100644
index a436e5a..0000000
--- a/src/main/scala/eu/xeppaka/bot/DialogManager.scala
+++ /dev/null
@@ -1,54 +0,0 @@
-package eu.xeppaka.bot
-
-import akka.actor.typed.scaladsl.Behaviors
-import akka.actor.typed.{ActorRef, Behavior}
-import akka.util.Timeout
-import eu.xeppaka.bot.CheckDeliveryDialog.{ProcessMessageFailure, ProcessMessageSuccess}
-import eu.xeppaka.telegram.bot.TelegramEntities._
-
-import scala.concurrent.duration._
-import scala.util.{Failure, Success}
-
-object DialogManager {
- sealed trait Command
- sealed trait CommandResult
-
- case class ProcessUpdate(update: Update, replyTo: ActorRef[CommandResult]) extends Command
- case object ProcessUpdateSuccess extends CommandResult
- case class ProcessUpdateFailure(exception: Throwable) extends CommandResult
-
- // internal messages
- private case class DialogResponseSuccess(dialogId: Long, replyTo: ActorRef[CommandResult]) extends Command
- private case class DialogResponseFailure(dialogId: Long, exception: Throwable, replyTo: ActorRef[CommandResult]) extends Command
-
- def behavior(botUri: BotUri): Behavior[Command] = Behaviors.setup[Command] { ctx =>
- Behaviors.receiveMessagePartial {
- case ProcessUpdate(update, replyTo) =>
- if (update.message.isDefined) {
- val chatId = update.message.get.chat.id
- ctx.log.debug("action=process_update chat_id={} message={}", chatId, update.message.get)
- val msg = update.message.get
- val dialogActor = ctx.child(chatId.toString).getOrElse(ctx.spawn(CheckDeliveryDialog.behavior(chatId, botUri), chatId.toString)).unsafeUpcast[CheckDeliveryDialog.Command]
- ctx.log.info("action=ask_dialog id={}", chatId)
-
- implicit val timeout: Timeout = 5.seconds
- ctx.ask[CheckDeliveryDialog.Command, CheckDeliveryDialog.CommandResult](dialogActor, replyTo => CheckDeliveryDialog.ProcessMessage(msg, replyTo)) {
- case Success(ProcessMessageSuccess) => DialogResponseSuccess(chatId, replyTo)
- case Success(ProcessMessageFailure(exception)) => DialogResponseFailure(chatId, exception, replyTo)
- case Failure(exception) => DialogResponseFailure(chatId, exception, replyTo)
- }
- } else {
- ctx.log.debug("action=process_update result=success message=update message is empty")
- }
- Behaviors.same
- case DialogResponseSuccess(dialogId, replyTo) =>
- ctx.log.info("action=ask_dialog id={} result=success", dialogId)
- replyTo ! ProcessUpdateSuccess
- Behaviors.same
- case DialogResponseFailure(dialogId, exception, replyTo) =>
- ctx.log.error(s"action=ask_dialog id=$dialogId result=failure", exception)
- replyTo ! ProcessUpdateFailure(exception)
- Behaviors.same
- }
- }
-}
diff --git a/src/main/scala/eu/xeppaka/bot/Main.scala b/src/main/scala/eu/xeppaka/bot/Main.scala
deleted file mode 100644
index 2f8c288..0000000
--- a/src/main/scala/eu/xeppaka/bot/Main.scala
+++ /dev/null
@@ -1,49 +0,0 @@
-package eu.xeppaka.bot
-
-import java.nio.file.Paths
-import akka.actor.Scheduler
-import akka.actor.typed.scaladsl.AskPattern._
-import akka.actor.typed.scaladsl.Behaviors
-import akka.actor.typed.scaladsl.adapter._
-import akka.actor.typed.{ ActorSystem, DispatcherSelector, SupervisorStrategy }
-import akka.http.scaladsl.Http
-import akka.util.Timeout
-import akka.{ actor, Done }
-import com.fasterxml.jackson.annotation.JsonInclude
-import com.fasterxml.jackson.databind.DeserializationFeature
-import de.heikoseeberger.akkahttpjackson.JacksonSupport
-
-import scala.concurrent.duration._
-import scala.concurrent.{ Await, ExecutionContextExecutor, Future }
-import scala.io.StdIn
-
-object Main {
- JacksonSupport.defaultObjectMapper.setSerializationInclusion(JsonInclude.Include.NON_EMPTY)
- JacksonSupport.defaultObjectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
-
- def main(args: Array[String]): Unit = {
- val botId = System.getProperty("botId", "570855144:AAEv7b817cuq2JJI9f2kG5B9G3zW1x-btz4")
- val localPort = 8443
- val hookDomain = System.getProperty("hookDomain", "xeppaka.eu")
- val hookPort = System.getProperty("hookPort", "8443").toInt
- val useHttpsServer = System.getProperty("useHttpsServer", "true").toBoolean
-
- val botBehavior = Behaviors.supervise(TelegramBot.behavior(botId, "0.0.0.0", localPort, hookDomain, hookPort, useHttpsServer)).onFailure(SupervisorStrategy.restart)
- val telegramBot = ActorSystem(botBehavior, "telegram-bot-delivery")
-// implicit val actorSystem: actor.ActorSystem = telegramBot.toUntyped
-// implicit val executionContext: ExecutionContextExecutor = telegramBot.dispatchers.lookup(DispatcherSelector.default())
-// implicit val scheduler: Scheduler = telegramBot.scheduler
-// implicit val timeout: Timeout = 10.seconds
-
-// println("Press enter to finish bot...")
-// StdIn.readLine()
-//
-// val stopFuture: Future[Done] = telegramBot ? (ref => TelegramBot.Stop(ref))
-//
-// val terminateFuture = stopFuture
-// .andThen { case _ => Http().shutdownAllConnectionPools() }
-// .andThen { case _ => telegramBot.terminate() }
-//
-// Await.ready(terminateFuture, 20.seconds)
- }
-}
diff --git a/src/main/scala/eu/xeppaka/bot/TelegramBot.scala b/src/main/scala/eu/xeppaka/bot/TelegramBot.scala
deleted file mode 100644
index bde41f9..0000000
--- a/src/main/scala/eu/xeppaka/bot/TelegramBot.scala
+++ /dev/null
@@ -1,252 +0,0 @@
-package eu.xeppaka.bot
-
-import java.io.InputStream
-import java.security.{ KeyStore, SecureRandom }
-import java.util.UUID
-
-import akka.Done
-import akka.actor.ActorSystem
-import akka.actor.typed.scaladsl.Behaviors
-import akka.actor.typed.scaladsl.adapter._
-import akka.actor.typed._
-import akka.http.scaladsl.marshalling.Marshal
-import akka.http.scaladsl.model._
-import akka.http.scaladsl.server.Directives.{ as, complete, entity, extractLog, onComplete, path, post }
-import akka.http.scaladsl.server.Route
-import akka.http.scaladsl.{ ConnectionContext, Http, HttpExt, HttpsConnectionContext }
-import akka.util.{ ByteString, Timeout }
-import eu.xeppaka.telegram.bot.TelegramEntities._
-import javax.net.ssl.{ KeyManagerFactory, SSLContext, TrustManagerFactory }
-
-import scala.collection.immutable
-import scala.concurrent.ExecutionContextExecutor
-import scala.concurrent.duration._
-import scala.io.Source
-import scala.util.{ Failure, Success }
-
-object TelegramBot {
- sealed trait Command
- sealed trait CommandResult
- sealed trait StopResult extends CommandResult
-
- case class Stop(replyTo: ActorRef[Done]) extends Command
- case object GetBotInfo
- case object GetWebhookInfo
-
- def behavior(botId: String, interface: String, localPort: Int, hookDomain: String, hookPort: Int, useHttpsServer: Boolean = true): Behavior[Command] = Behaviors.withStash(100) { stashBuffer =>
- Behaviors.setup[Command] { ctx =>
- ctx.log.info("action=start_bot")
-
- implicit val untypedSystem: ActorSystem = ctx.system.toClassic
- implicit val executionContextExecutor: ExecutionContextExecutor = ctx.system.dispatchers.lookup(DispatcherSelector.default())
-
- val botUri = BotUri(botId)
- val http: HttpExt = Http()
- val hookId = UUID.randomUUID().toString
- val webhookUri = Uri(s"https://$hookDomain:$hookPort/$hookId")
- val dialogManager = ctx.spawnAnonymous(Behaviors.supervise(DialogManager.behavior(botUri)).onFailure(SupervisorStrategy.restart))
- val routes = botRoutes(hookId, dialogManager)(ctx.system.scheduler)
-
- def bindServer: Behavior[Command] = Behaviors.setup[Command] { ctx =>
- case class BindingSuccess(binding: Http.ServerBinding) extends Command
- case class BindingFailure(exception: Throwable) extends Command
-
- ctx.log.info("action=bind_server interface={} port={}", interface, localPort)
-
- val serverBuilder = http.newServerAt(interface, localPort)
- val bindFuture = if (useHttpsServer) {
- serverBuilder.enableHttps(createHttpsConnectionContext).bindFlow(routes)
- } else {
- serverBuilder.bindFlow(routes)
- }
-
- bindFuture.onComplete {
- case Success(binding) => ctx.self ! BindingSuccess(binding)
- case Failure(exception) => ctx.self ! BindingFailure(exception)
- }
-
- Behaviors.receiveMessage[Command] {
- case BindingSuccess(binding) =>
- ctx.log.info("action=bind_server result=success")
- setWebhook(binding)
- case BindingFailure(exception) =>
- ctx.log.error("action=bind_server result=failure", exception)
- ctx.log.error("action=start_bot result=failure")
- Behaviors.stopped
- case otherCommand: Command =>
- stashBuffer.stash(otherCommand)
- Behaviors.same
- }
- }
-
- def unbindServer(binding: Http.ServerBinding, replyTo: Option[ActorRef[Done]]): Behavior[Command] = Behaviors.setup[Command] { ctx =>
- case object UnbindingSuccess extends Command
- case class UnbindingFailure(exception: Throwable) extends Command
-
- ctx.log.info("action=unbind_server interface={} port={}", interface, localPort)
-
- binding.unbind().onComplete {
- case Success(Done) => ctx.self ! UnbindingSuccess
- case Failure(exception) => ctx.self ! UnbindingFailure(exception)
- }
-
- Behaviors.receiveMessage[Command] {
- case UnbindingSuccess =>
- ctx.log.info("action=unbind_server result=success")
- replyTo.foreach(_ ! Done)
- Behaviors.stopped
- case UnbindingFailure(exception) =>
- ctx.log.error("action=unbind_server result=failure", exception)
- replyTo.foreach(_ ! Done)
- Behaviors.stopped
- case _ => Behaviors.unhandled
- }
- }
-
- def setWebhook(binding: Http.ServerBinding, attempt: Int = 1): Behavior[Command] = Behaviors.setup[Command] { ctx =>
- case object SetWebhookSuccess extends Command
- case class SetWebhookFailure(exception: Throwable) extends Command
-
- ctx.log.info("action=set_webhook url={} webhook={}", botUri.setWebhook, webhookUri)
-
- implicit val executionContextExecutor: ExecutionContextExecutor = ctx.system.dispatchers.lookup(DispatcherSelector.default())
-
- val urlEntity = HttpEntity.Strict(ContentTypes.`text/plain(UTF-8)`, ByteString(webhookUri.toString()))
- val urlPart = Some(Multipart.FormData.BodyPart.Strict("url", urlEntity))
-
- val certificatePart = if (useHttpsServer) {
- val certificate = ByteString(Source.fromResource("telegram-bot.pem").mkString)
- val certificateEntity = HttpEntity.Strict(ContentTypes.`application/octet-stream`, certificate)
-
- Some(Multipart.FormData.BodyPart.Strict("certificate", certificateEntity, Map("filename" -> "cert.pem")))
- } else {
- None
- }
-
- val formParts = immutable.Seq(urlPart, certificatePart).flatten
- val formData = Multipart.FormData.Strict(formParts)
-
- Marshal(formData).to[RequestEntity].flatMap(requestEntity => http.singleRequest(HttpRequest(uri = botUri.setWebhook, method = HttpMethods.POST, entity = requestEntity))).onComplete {
- case Success(response) =>
- if (response.status.isSuccess())
- ctx.self ! SetWebhookSuccess
- else
- ctx.self ! SetWebhookFailure(new RuntimeException(s"Set webhook HTTP response status: ${response.status.value}."))
- case Failure(exception) =>
- ctx.self ! SetWebhookFailure(exception)
- }
-
- Behaviors.receiveMessage {
- case SetWebhookSuccess =>
- ctx.log.info("action=set_webhook result=success")
- stashBuffer.unstashAll(started(binding))
- case SetWebhookFailure(exception) =>
- if (attempt > 20) {
- ctx.log.error(s"action=set_webhook result=failure attempt=$attempt", exception)
- ctx.log.error("action=start_bot result=failure")
- unbindServer(binding, None)
- } else {
- setWebhook(binding, attempt = attempt + 1)
- }
- case otherCommand: Command =>
- stashBuffer.stash(otherCommand)
- Behaviors.same
- }
- }
-
- def deletingWebhook(binding: Http.ServerBinding, replyTo: ActorRef[Done]): Behavior[Command] = Behaviors.setup[Command] { ctx =>
- case object DeleteWebhookSuccess extends Command
- case class DeleteWebhookFailure(exception: Throwable) extends Command
-
- ctx.log.info("action=delete_webhook url={} webhook={}", botUri.deleteWebhook, webhookUri)
-
- implicit val executionContextExecutor: ExecutionContextExecutor = ctx.system.dispatchers.lookup(DispatcherSelector.default())
-
- http.singleRequest(HttpRequest(uri = botUri.deleteWebhook, method = HttpMethods.POST)).onComplete {
- case Success(response) =>
- if (response.status.isSuccess())
- ctx.self ! DeleteWebhookSuccess
- else
- ctx.self ! DeleteWebhookFailure(new RuntimeException(s"Delete webhook HTTP response status: ${response.status.value}"))
- case Failure(exception) =>
- ctx.self ! DeleteWebhookFailure(exception)
- }
-
- Behaviors.receiveMessage {
- case DeleteWebhookSuccess =>
- ctx.log.info("action=delete_webhook result=success")
- unbindServer(binding, Some(replyTo))
- case DeleteWebhookFailure(exception) =>
- ctx.log.error("action=delete_webhook result=failure", exception)
- unbindServer(binding, Some(replyTo))
- case _ => Behaviors.unhandled
- }
- }
-
- def started(binding: Http.ServerBinding): Behavior[Command] = Behaviors.setup[Command] { ctx =>
- ctx.log.info("action=start_bot result=success")
-
- Behaviors.receiveMessage[Command] {
- case Stop(replyTo) =>
- ctx.log.info("action=stop_bot")
- deletingWebhook(binding, replyTo)
- case _ =>
- Behaviors.unhandled
- }
- }
-
- bindServer
- }
- }
-
- private def botRoutes(hookId: String, updatesProcessor: ActorRef[DialogManager.ProcessUpdate])(implicit scheduler: Scheduler): Route = {
- import de.heikoseeberger.akkahttpjackson.JacksonSupport._
- import akka.actor.typed.scaladsl.AskPattern._
-
- implicit val timeout: Timeout = 30.seconds
-
- path(hookId) {
- post {
- extractLog { log =>
- entity(as[Update]) { update =>
-// log.info("update={}", update)
-// complete(StatusCodes.OK)
- onComplete(updatesProcessor.ask[DialogManager.CommandResult](ref => DialogManager.ProcessUpdate(update, ref))) {
- case Success(processResult) =>
- processResult match {
- case DialogManager.ProcessUpdateSuccess => complete(HttpResponse(status = StatusCodes.OK))
- case DialogManager.ProcessUpdateFailure(exception) =>
- log.error(exception, "action=process_update result=failure message={}", update)
- complete(HttpResponse(status = StatusCodes.InternalServerError))
- }
- case Failure(exception) =>
- log.error(exception, "action=process_update result=failure message={}", update)
- complete(HttpResponse(status = StatusCodes.InternalServerError))
- }
- }
- }
- }
- }
- }
-
- private def createHttpsConnectionContext: HttpsConnectionContext = {
- val password: Array[Char] = "".toCharArray // do not store passwords in code, read them from somewhere safe!
-
- val ks: KeyStore = KeyStore.getInstance("PKCS12")
- val keystore: InputStream = getClass.getResourceAsStream("/telegram-bot.p12")
-
- require(keystore != null, "Keystore required!")
- ks.load(keystore, password)
-
- val keyManagerFactory: KeyManagerFactory = KeyManagerFactory.getInstance("SunX509")
- keyManagerFactory.init(ks, password)
-
- val tmf: TrustManagerFactory = TrustManagerFactory.getInstance("SunX509")
- tmf.init(ks)
-
- val sslContext: SSLContext = SSLContext.getInstance("TLS")
- sslContext.init(keyManagerFactory.getKeyManagers, tmf.getTrustManagers, new SecureRandom)
-
- ConnectionContext.https(sslContext)
- }
-}
diff --git a/src/main/scala/tech/xeppaka/bot/BotUri.scala b/src/main/scala/tech/xeppaka/bot/BotUri.scala
new file mode 100644
index 0000000..71d8a56
--- /dev/null
+++ b/src/main/scala/tech/xeppaka/bot/BotUri.scala
@@ -0,0 +1,16 @@
+package eu.xeppaka.bot
+
+import sttp.client3._
+import sttp.model.Uri
+
+case class BotUri(botId: String) {
+ private val baseUri = uri"https://api.telegram.org/bot$botId"
+
+ val botUri: Uri = baseUri
+ val getMe: Uri = baseUri.addPath("getMe")
+ val setWebhook: Uri = baseUri.addPath("setWebhook")
+ val deleteWebhook: Uri = baseUri.addPath("deleteWebhook")
+ val getWebhookInfo: Uri = baseUri.addPath("getWebhookInfo")
+ val sendMessage: Uri = baseUri.addPath("sendMessage")
+ val editMessageReplyMarkup: Uri = baseUri.addPath("editMessageReplyMarkup")
+}
diff --git a/src/main/scala/tech/xeppaka/bot/CheckDeliveryDialog.scala b/src/main/scala/tech/xeppaka/bot/CheckDeliveryDialog.scala
new file mode 100644
index 0000000..9cd0b61
--- /dev/null
+++ b/src/main/scala/tech/xeppaka/bot/CheckDeliveryDialog.scala
@@ -0,0 +1,302 @@
+package eu.xeppaka.bot
+
+// import akka.actor.ActorSystem
+// import akka.actor.typed.scaladsl.Behaviors
+// import akka.actor.typed.scaladsl.adapter._
+// import akka.http.scaladsl.marshalling.Marshal
+// import akka.actor.typed.{ActorRef, Behavior, SupervisorStrategy}
+// import akka.http.scaladsl.Http
+// import akka.http.scaladsl.model._
+// import akka.stream.scaladsl.{Sink, Source}
+// import akka.util.{ByteString, Timeout}
+// import eu.xeppaka.telegram.bot.TelegramEntities._
+
+// import scala.concurrent.{Await, ExecutionContext}
+// import scala.concurrent.duration._
+// import scala.util.{Failure, Success}
+
+object CheckDeliveryDialog {
+ // import de.heikoseeberger.akkahttpjackson.JacksonSupport._
+
+ // sealed trait Command
+ // sealed trait CommandResult
+ // sealed trait DialogCommand extends Command
+
+ // case class ProcessMessage(msg: Message, replyTo: ActorRef[CommandResult]) extends Command
+ // case object ProcessMessageSuccess extends CommandResult
+ // case class ProcessMessageFailure(exception: Throwable) extends CommandResult
+
+ // case object AddParcel extends DialogCommand
+ // case object RemoveParcel extends DialogCommand
+ // case object ListParcels extends DialogCommand
+ // case object Help extends DialogCommand
+
+ // object DialogCommand {
+ // def parse(text: String): DialogCommand = text match {
+ // case "/add" => AddParcel
+ // case "/remove" => RemoveParcel
+ // case "/list" => ListParcels
+ // case "/help" => Help
+ // case _ => Help
+ // }
+ // }
+
+ // // internal messages
+ // private case class DeliveryStateChanged(state: String) extends Command
+ // private val helpMessage =
+ // """
+ // |Supported commands:
+ // |/add - add parcel to a list of watched parcels
+ // |/list - list watched parcels
+ // |/remove - remove parcel from a watching list
+ // """.stripMargin
+ // private val commandsKeyboard = Some(
+ // ReplyKeyboardMarkup(Seq(Seq(KeyboardButton("/add"), KeyboardButton("/list"), KeyboardButton("/remove"))), resize_keyboard = Some(true), one_time_keyboard = Some(true))
+ // )
+
+ // private val removeKeyboard = Some(ReplyKeyboardRemove())
+
+ // def behavior(chatId: Long, botUri: BotUri): Behavior[Command] = Behaviors.setup[Command] { ctx =>
+ // implicit val system: ActorSystem = ctx.system.toClassic
+ // implicit val executionContext: ExecutionContext = ctx.executionContext
+ // val http = Http()
+ // val deliveryStateAdapter: ActorRef[CzechPostDeliveryCheck.DeliveryStateChanged] = ctx.messageAdapter(stateChanged => DeliveryStateChanged(stateChanged.state))
+ // val czechPostDeliveryCheck = ctx.spawnAnonymous(Behaviors.supervise(CzechPostDeliveryCheck.behavior(chatId.toString, deliveryStateAdapter)).onFailure(SupervisorStrategy.restart))
+
+ // def waitCommand: Behavior[Command] = Behaviors.receiveMessage {
+ // case ProcessMessage(msg, replyTo) =>
+ // val command = msg.text.map(text => DialogCommand.parse(text))
+ // replyTo ! ProcessMessageSuccess
+
+ // if (command.isDefined) {
+ // ctx.self ! command.get
+ // Behaviors.same
+ // } else {
+ // val message = SendMessage(chatId, "This command is unsupported.")
+ // sendMessage(message, waitCommand, waitCommand)
+ // }
+ // case AddParcel =>
+ // val parcelIdMessage = SendMessage(chatId, "Please enter a parcel ID.", reply_markup = removeKeyboard)
+ // val commentMessage = SendMessage(chatId, "Please enter a comment.", reply_markup = removeKeyboard)
+ // sendMessage(parcelIdMessage, waitTextMessage(parcelId => sendMessage(commentMessage, waitTextMessage(comment => addParcel(parcelId, comment)), waitCommand)), waitCommand)
+ // case RemoveParcel =>
+ // removeParcel(waitCommand, waitCommand)
+ // case ListParcels =>
+ // listParcels
+ // case Help =>
+ // val message = SendMessage(chatId, helpMessage, reply_markup = commandsKeyboard)
+ // sendMessage(message, waitCommand, waitCommand)
+ // case DeliveryStateChanged(state) =>
+ // val message = SendMessage(chatId, state, Some("Markdown"))
+ // sendMessage(message, waitCommand, waitCommand)
+ // case _ =>
+ // Behaviors.unhandled
+ // }
+
+ // def addParcel(parcelId: String, comment: String): Behavior[Command] = Behaviors.withStash(100) { stashBuffer =>
+ // Behaviors.setup { ctx =>
+ // case object AddParcelSuccess extends Command
+ // case class AddParcelFailure(exception: Throwable) extends Command
+ // implicit val timeout: Timeout = 5.seconds
+
+ // ctx.ask[CzechPostDeliveryCheck.Command, CzechPostDeliveryCheck.CommandResult](czechPostDeliveryCheck, ref => CzechPostDeliveryCheck.AddParcel(parcelId, comment, ref)) {
+ // case Success(CzechPostDeliveryCheck.CommandResultSuccess) => AddParcelSuccess
+ // case Success(CzechPostDeliveryCheck.CommandResultFailure(exception)) => AddParcelFailure(exception)
+ // case Failure(exception) => AddParcelFailure(exception)
+ // }
+
+ // Behaviors.receiveMessage {
+ // case AddParcelSuccess =>
+ // val message = SendMessage(chatId, s"Parcel $parcelId was added to the watch list.", reply_markup = commandsKeyboard)
+ // sendMessage(message, waitCommand, waitCommand)
+ // case AddParcelFailure(exception) =>
+ // exception match {
+ // case CzechPostDeliveryCheck.DuplicateParcelId(_) =>
+ // val message = SendMessage(chatId, s"Parcel $parcelId is in the watch list already.", reply_markup = commandsKeyboard)
+ // sendMessage(message, waitCommand, waitCommand)
+ // case _ =>
+ // ctx.log.error("action=add_parcel result=failure", exception)
+ // val message = SendMessage(chatId, s"Adding parcel failed. Please try again.", reply_markup = commandsKeyboard)
+ // sendMessage(message, waitCommand, waitCommand)
+ // }
+ // case otherMessage =>
+ // stashBuffer.stash(otherMessage)
+ // Behaviors.same
+ // }
+ // }
+ // }
+
+ // def listParcels: Behavior[Command] = Behaviors.withStash(100) { stashBuffer =>
+ // Behaviors.setup { ctx =>
+ // case class ListParcelsSuccess(parcelsList: Seq[String]) extends Command
+ // case class ListParcelsFailure(exception: Throwable) extends Command
+ // implicit val timeout: Timeout = 5.seconds
+
+ // ctx.ask[CzechPostDeliveryCheck.Command, CzechPostDeliveryCheck.ListParcelsResult](czechPostDeliveryCheck, ref => CzechPostDeliveryCheck.ListParcels(ref)) {
+ // case Success(CzechPostDeliveryCheck.ListParcelsResult(parcelsList)) => ListParcelsSuccess(parcelsList)
+ // case Failure(exception) => ListParcelsFailure(exception)
+ // }
+
+ // Behaviors.receiveMessage {
+ // case ListParcelsSuccess(parcelsList) =>
+ // val messageText = "*List of your watched parcels:*\n" + (if (parcelsList.nonEmpty) parcelsList.sorted.mkString("\n") else "(empty)")
+ // val message = SendMessage(chatId, messageText, Some("Markdown"), reply_markup = commandsKeyboard)
+ // sendMessage(message, waitCommand, waitCommand)
+ // case ListParcelsFailure(exception) =>
+ // ctx.log.error(s"action=list_parcels result=failure chat_id=$chatId", exception)
+ // val message = SendMessage(chatId, "Failed to get a list of your watched parcels. Please try again later.", reply_markup = commandsKeyboard)
+ // sendMessage(message, waitCommand, waitCommand)
+ // case otherMessage =>
+ // stashBuffer.stash(otherMessage)
+ // Behaviors.same
+ // }
+ // }
+ // }
+
+ // def removeParcel(onSuccess: => Behavior[Command], onFailure: => Behavior[Command]): Behavior[Command] = Behaviors.withStash(100) { stashBuffer =>
+ // Behaviors.setup { ctx =>
+ // case class ListParcelIdsSuccess(parcelsList: Seq[String]) extends Command
+ // case class ListParcelIdsFailure(exception: Throwable) extends Command
+ // implicit val timeout: Timeout = 5.seconds
+
+ // ctx.ask[CzechPostDeliveryCheck.Command, CzechPostDeliveryCheck.ListParcelIdsResult](czechPostDeliveryCheck, ref => CzechPostDeliveryCheck.ListParcelIds(ref)) {
+ // case Success(CzechPostDeliveryCheck.ListParcelIdsResult(parcelsList)) => ListParcelIdsSuccess(parcelsList)
+ // case Failure(exception) => ListParcelIdsFailure(exception)
+ // }
+
+ // Behaviors.receiveMessage {
+ // case ListParcelIdsSuccess(parcelsList) =>
+ // if (parcelsList.nonEmpty) {
+ // val keyboardButtons = parcelsList.sorted.grouped(3).map(_.map(id => KeyboardButton(id))).toSeq
+ // val markup = ReplyKeyboardMarkup(keyboard = keyboardButtons, resize_keyboard = Some(true), one_time_keyboard = Some(true))
+ // val message = SendMessage(chatId, "Please enter a parcel id to remove.", reply_markup = Some(markup))
+ // sendMessage(message, waitTextMessage(parcelId => removeParcelId(parcelId)), onFailure)
+ // } else {
+ // val message = SendMessage(chatId, "You don't have watched parcels. There is nothing to remove.", reply_markup = commandsKeyboard)
+ // sendMessage(message, onSuccess, onFailure)
+ // }
+ // case ListParcelIdsFailure(exception) =>
+ // ctx.log.error(s"action=list_parcels result=failure chat_id=$chatId", exception)
+ // val message = SendMessage(chatId, "Failed to get a list of your watched parcels. Please try again later.", reply_markup = commandsKeyboard)
+ // sendMessage(message, waitCommand, waitCommand)
+ // case otherMessage =>
+ // stashBuffer.stash(otherMessage)
+ // Behaviors.same
+ // }
+ // }
+ // }
+
+ // def removeParcelId(parcelId: String): Behavior[Command] = Behaviors.withStash(100) { stashBuffer =>
+ // Behaviors.setup { ctx =>
+ // case object RemoveParcelSuccess extends Command
+ // case class RemoveParcelFailure(exception: Throwable) extends Command
+ // implicit val timeout: Timeout = 5.seconds
+
+ // ctx.ask[CzechPostDeliveryCheck.Command, CzechPostDeliveryCheck.CommandResult](czechPostDeliveryCheck, ref => CzechPostDeliveryCheck.RemoveParcel(parcelId, ref)) {
+ // case Success(CzechPostDeliveryCheck.CommandResultSuccess) => RemoveParcelSuccess
+ // case Success(CzechPostDeliveryCheck.CommandResultFailure(exception)) => RemoveParcelFailure(exception)
+ // case Failure(exception) => RemoveParcelFailure(exception)
+ // }
+
+ // Behaviors.receiveMessage {
+ // case RemoveParcelSuccess =>
+ // val message = SendMessage(chatId, s"Parcel $parcelId was removed from the watch list.", reply_markup = commandsKeyboard)
+ // sendMessage(message, waitCommand, waitCommand)
+ // case RemoveParcelFailure(exception) =>
+ // exception match {
+ // case CzechPostDeliveryCheck.ParcelIdNotFound(_) =>
+ // val message = SendMessage(chatId, s"Parcel $parcelId is not found in the list of the watched parcels.", reply_markup = commandsKeyboard)
+ // sendMessage(message, waitCommand, waitCommand)
+ // case _ =>
+ // ctx.log.error("action=add_parcel result=failure", exception)
+ // val message = SendMessage(chatId, s"Remove of the parcel failed. Please try again.", reply_markup = commandsKeyboard)
+ // sendMessage(message, waitCommand, waitCommand)
+ // }
+ // case otherMessage =>
+ // stashBuffer.stash(otherMessage)
+ // Behaviors.same
+ // }
+ // }
+ // }
+
+ // // def selectPostType(onFinish: PostType => Behavior[Command]): Behavior[Command] = Behaviors.receiveMessage {
+ // //
+ // // case ProcessMessage(msg, replyTo) =>
+ // // val button1 = KeyboardButton("button1")
+ // // val button2 = KeyboardButton("button2")
+ // // val keyboard = ReplyKeyboardMarkup(Seq(Seq(button1, button2)))
+ // // val message = SendMessage(chatId, "Please enter parcel ID.", reply_markup = Some(keyboard))
+ // // sendMessage(message, waitParcelId(parcelId => addParcel(parcelId)), waitCommand)
+ // // }
+
+ // def waitTextMessage(onFinish: String => Behavior[Command]): Behavior[Command] = Behaviors.withStash(100) { stashBuffer =>
+ // Behaviors.receiveMessage {
+ // case ProcessMessage(msg, replyTo) =>
+ // if (msg.text.isDefined) {
+ // val parcelId = msg.text.get
+ // replyTo ! ProcessMessageSuccess
+ // onFinish(parcelId)
+ // } else {
+ // replyTo ! ProcessMessageSuccess
+ // waitTextMessage(onFinish)
+ // }
+ // case otherMsg =>
+ // stashBuffer.stash(otherMsg)
+ // Behaviors.same
+ // }
+ // }
+
+ // def sendMessage(message: SendMessage, onSuccess: => Behavior[Command], onFailure: => Behavior[Command], attempt: Int = 1): Behavior[Command] = Behaviors.withStash(100) { stashBuffer =>
+ // Behaviors.setup[Command] { ctx =>
+
+ // case object SendMessageSuccess extends Command
+ // case class SendMessageFailure(exception: Throwable) extends Command
+
+ // ctx.log.debug("action=send_message status=started chat_id={} message={}", chatId, message)
+
+ // println(message)
+ // println(Await.result(Marshal(message).to[HttpEntity], 2.seconds).asInstanceOf[HttpEntity.Strict].data.utf8String)
+
+ // Source
+ // .future(Marshal(message).to[RequestEntity])
+ // .initialDelay(2.seconds * (attempt - 1))
+ // .map(requestEntity => HttpRequest(HttpMethods.POST, uri = botUri.sendMessage, entity = requestEntity))
+ // .mapAsync(1) { request =>
+ // http.singleRequest(request).transform {
+ // case Success(response) =>
+ // if (response.status.isSuccess()) {
+ // Success(SendMessageSuccess)
+ // } else {
+ // Success(SendMessageFailure(new RuntimeException(s"Error while sending message. HTTP status: ${response.status}.")))
+ // }
+ // case Failure(exception) =>
+ // ctx.log.error(s"action=send_message status=finished result=failure chat_id=$chatId", exception)
+ // Success(SendMessageFailure(exception))
+ // }
+ // }
+ // .to(Sink.foreach(ctx.self ! _))
+ // .run()
+
+ // Behaviors.receiveMessage {
+ // case SendMessageSuccess =>
+ // ctx.log.debug("action=send_message status=finished result=success chat_id={}", chatId)
+ // stashBuffer.unstashAll(onSuccess)
+ // case SendMessageFailure(exception) =>
+ // ctx.log.error(s"action=send_message status=finished result=failure chat_id=$chatId attempt=$attempt", exception)
+
+ // if (attempt > 5) {
+ // ctx.log.error("action=send_message result=failure message=attempts threshold exceeded", exception)
+ // stashBuffer.unstashAll(onFailure)
+ // } else {
+ // sendMessage(message, onSuccess, onFailure, attempt + 1)
+ // }
+ // case otherMsg =>
+ // stashBuffer.stash(otherMsg)
+ // Behaviors.same
+ // }
+ // }
+ // }
+
+ // waitCommand
+ // }
+}
diff --git a/src/main/scala/tech/xeppaka/bot/CzechPostDeliveryCheck.scala b/src/main/scala/tech/xeppaka/bot/CzechPostDeliveryCheck.scala
new file mode 100644
index 0000000..d760ef3
--- /dev/null
+++ b/src/main/scala/tech/xeppaka/bot/CzechPostDeliveryCheck.scala
@@ -0,0 +1,248 @@
+package eu.xeppaka.bot
+
+// import akka.actor.ActorSystem
+// import akka.actor.typed.scaladsl.Behaviors
+// import akka.actor.typed.scaladsl.adapter._
+// import akka.actor.typed.{ActorRef, Behavior, DispatcherSelector}
+// import akka.http.scaladsl.model._
+// import akka.http.scaladsl.model.headers.{`User-Agent`, Accept}
+// import akka.http.scaladsl.settings.{ClientConnectionSettings, ConnectionPoolSettings}
+// import akka.http.scaladsl.unmarshalling.Unmarshal
+// import akka.http.scaladsl.{ConnectionContext, Http}
+// import akka.persistence.typed.PersistenceId
+// import akka.persistence.typed.scaladsl.EventSourcedBehavior.{CommandHandler, EventHandler}
+// import akka.persistence.typed.scaladsl.{Effect, EventSourcedBehavior}
+// import com.fasterxml.jackson.annotation.{JsonSubTypes, JsonTypeInfo}
+// import com.typesafe.sslconfig.akka.AkkaSSLConfig
+
+// import java.security.cert.X509Certificate
+// import java.text.SimpleDateFormat
+// import javax.net.ssl.{KeyManager, SSLContext, X509TrustManager}
+// import scala.collection.immutable
+// import scala.concurrent.ExecutionContextExecutor
+// import scala.concurrent.duration._
+// import scala.util.{Failure, Success}
+
+object Entities {
+
+// case class Attributes(parcelType: String, weight: Double, currency: String)
+
+// case class State(
+// id: String,
+// date: String,
+// text: String,
+// postcode: Option[String],
+// postoffice: Option[String],
+// idIcon: Option[Int],
+// publicAccess: Int,
+// latitude: Option[Double],
+// longitude: Option[Double],
+// timeDeliveryAttempt: Option[String]
+// )
+
+// case class States(state: Seq[State])
+
+// case class ParcelHistory(id: String, attributes: Attributes, states: States)
+// }
+
+// object CzechPostDeliveryCheck {
+// import de.heikoseeberger.akkahttpjackson.JacksonSupport._
+
+// private val czechPostDateFormat = new SimpleDateFormat("yyyy-MM-dd")
+// private val printDateFormat = new SimpleDateFormat("dd-MM-yyyy")
+// private val entityType = "czechpost"
+
+// sealed trait Command extends JsonSerializable
+// sealed trait CommandResult extends JsonSerializable
+// @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type")
+// @JsonSubTypes(
+// Array(
+// new JsonSubTypes.Type(value = classOf[ParcelAttributesChanged], name = "parcel_attributes_changed"),
+// new JsonSubTypes.Type(value = classOf[ParcelAdded], name = "parcel_added"),
+// new JsonSubTypes.Type(value = classOf[ParcelRemoved], name = "parcel_removed"),
+// new JsonSubTypes.Type(value = classOf[ParcelHistoryStateAdded], name = "parcel_history_state_added")
+// )
+// )
+// sealed trait Event extends JsonSerializable
+// case class Parcel(comment: String, attributes: Option[Entities.Attributes] = None, states: Set[Entities.State] = Set.empty) {
+// def fullStatePrint(parcelId: String): String = {
+// val statesString = states.toSeq
+// .sortBy(state => czechPostDateFormat.parse(state.date))
+// .map(state => s"${printDateFormat.format(czechPostDateFormat.parse(state.date))} - ${state.text}\n===========================\n")
+// .mkString
+
+// s"""|*New state(s) of the parcel $parcelId ($comment):*
+// |===========================
+// |$statesString""".stripMargin
+// }
+
+// def latestStatePrint(parcelId: String): String = {
+// latestState.map(state => s"$parcelId ($comment) - ${printDateFormat.format(czechPostDateFormat.parse(state.date))} - ${state.text}").getOrElse(s"$parcelId ($comment) - NO INFO")
+// }
+
+// private def latestState: Option[Entities.State] = states.toSeq.maxByOption(state => czechPostDateFormat.parse(state.date))
+// }
+
+// case class State(parcelStates: Map[String, Parcel] = Map.empty) extends JsonSerializable {
+// def latestStatesPrint: Seq[String] = parcelStates.map { case (id, parcel) => parcel.latestStatePrint(id) }.to(Seq)
+// }
+
+// case class AddParcel(parcelId: String, comment: String, replyTo: ActorRef[CommandResult]) extends Command
+// case class RemoveParcel(parcelId: String, replyTo: ActorRef[CommandResult]) extends Command
+// case class ListParcels(replyTo: ActorRef[ListParcelsResult]) extends Command
+// case class ListParcelsResult(parcelsList: Seq[String])
+// case class ListParcelIds(replyTo: ActorRef[ListParcelIdsResult]) extends Command
+// case class ListParcelIdsResult(parcelIds: Seq[String])
+
+// case object CommandResultSuccess extends CommandResult
+// case class CommandResultFailure(exception: Throwable) extends CommandResult
+
+// case class ParcelIdNotFound(parcelId: String) extends Exception
+// case class DuplicateParcelId(parcelId: String) extends Exception
+
+// // internal commands
+// private case object CheckParcels extends Command
+// private case class ParcelHistoryRetrieved(parcelHistory: Entities.ParcelHistory) extends Command
+// case class DeliveryStateChanged(state: String)
+
+// case class ParcelAdded(parcelId: String, comment: String) extends Event
+// case class ParcelRemoved(parcelId: String) extends Event
+// case class ParcelHistoryStateAdded(parcelId: String, state: Entities.State) extends Event
+// case class ParcelAttributesChanged(parcelId: String, attributes: Entities.Attributes) extends Event
+
+// private val trustfulSslContext: SSLContext = {
+// object NoCheckX509TrustManager extends X509TrustManager {
+// override def checkClientTrusted(chain: Array[X509Certificate], authType: String): Unit = ()
+// override def checkServerTrusted(chain: Array[X509Certificate], authType: String): Unit = ()
+// override def getAcceptedIssuers: Array[X509Certificate] = Array[X509Certificate]()
+// }
+
+// val context = SSLContext.getInstance("TLS")
+// context.init(Array[KeyManager](), Array(NoCheckX509TrustManager), null)
+// context
+// }
+
+// def behavior(chatId: String, stateReporter: ActorRef[DeliveryStateChanged]): Behavior[Command] = checkParcel(chatId, stateReporter)
+
+// private def checkParcel(chatId: String, stateReporter: ActorRef[DeliveryStateChanged]): Behavior[Command] = Behaviors.withTimers { scheduler =>
+// Behaviors.setup { ctx =>
+// implicit val actorSystem: ActorSystem = ctx.system.toClassic
+// implicit val executionContext: ExecutionContextExecutor = ctx.system.dispatchers.lookup(DispatcherSelector.default())
+// val http = Http()
+// val badSslConfig = AkkaSSLConfig().mapSettings(s => s.withLoose(s.loose.withAcceptAnyCertificate(true).withDisableHostnameVerification(true)))
+// val originalCtx = http.createClientHttpsContext(badSslConfig)
+// val sslContext = ConnectionContext.httpsClient(trustfulSslContext)
+// val clientConnectionSettings = ClientConnectionSettings(actorSystem).withUserAgentHeader(Some(`User-Agent`("Mozilla/5.0 (X11; Linux x86_64; rv:62.0) Gecko/20100101 Firefox/62.0")))
+// val connectionSettings = ConnectionPoolSettings(actorSystem).withConnectionSettings(clientConnectionSettings)
+
+// scheduler.startTimerAtFixedRate("check-delivery-state", CheckParcels, 5.minutes)
+
+// val log = ctx.log
+
+// val commandHandler: CommandHandler[Command, Event, State] = (state, cmd) => {
+// cmd match {
+// case AddParcel(parcelId, comment, replyTo) =>
+// val parcelIdUpper = parcelId.toUpperCase
+// if (state.parcelStates.keySet.contains(parcelIdUpper)) {
+// Effect.none.thenRun(_ => replyTo ! CommandResultFailure(DuplicateParcelId(parcelIdUpper)))
+// } else {
+// Effect
+// .persist(ParcelAdded(parcelIdUpper, comment))
+// .thenRun(_ => {
+// replyTo ! CommandResultSuccess
+// ctx.self ! CheckParcels
+// })
+// }
+// case RemoveParcel(parcelId, replyTo) =>
+// val parcelIdUpper = parcelId.toUpperCase
+// if (state.parcelStates.contains(parcelIdUpper)) {
+// Effect.persist(ParcelRemoved(parcelIdUpper)).thenRun(_ => replyTo ! CommandResultSuccess)
+// } else {
+// Effect.none.thenRun(_ => replyTo ! CommandResultFailure(ParcelIdNotFound(parcelIdUpper)))
+// }
+
+// case ListParcels(replyTo) =>
+// Effect.none.thenRun { state =>
+// val parcelsList = state.latestStatesPrint
+// replyTo ! ListParcelsResult(parcelsList)
+// }
+
+// case ListParcelIds(replyTo) =>
+// Effect.none.thenRun { state =>
+// replyTo ! ListParcelIdsResult(state.parcelStates.keys.toSeq)
+// }
+
+// case CheckParcels =>
+// Effect.none.thenRun { _ =>
+// log.info("action=check_parcel_state chat_id={}", chatId)
+// val parcelIds = state.parcelStates.keys.grouped(10).map(ids => ids.foldLeft("")((acc, id) => if (acc.isEmpty) id else s"$acc;$id"))
+
+// for (ids <- parcelIds) {
+// val checkUri = Uri(s"https://b2c.cpost.cz/services/ParcelHistory/getDataAsJson?idParcel=$ids&language=cz")
+// val request = HttpRequest(uri = checkUri, headers = immutable.Seq(Accept(MediaTypes.`application/json`)))
+
+// log.info("action=check_parcel_state chat_id={} check_uri={}", chatId, checkUri)
+
+// http
+// .singleRequest(request, connectionContext = sslContext, settings = connectionSettings)
+// .transform {
+// case Success(response) => if (response.status.isSuccess()) Success(response) else Failure(new Exception(s"Check parcel returned HTTP status: ${response.status.value}."))
+// case response: Failure[HttpResponse] => response
+// }
+// .flatMap(response => Unmarshal(response).to[Seq[Entities.ParcelHistory]])
+// .andThen {
+// case Success(parcelHistories) =>
+// parcelHistories.foreach(parcelHistory => ctx.self ! ParcelHistoryRetrieved(parcelHistory))
+// case Failure(exception) =>
+// log.error("Error checking parcel history.", exception)
+// }
+// .andThen {
+// case Success(_) => log.info("action=check_parcel_state result=success chat_id={} check_uri={}", chatId, checkUri)
+// case Failure(exception) => log.error(s"action=check_parcel_state result=failure chat_id=$chatId check_uri=$checkUri", exception)
+// }
+// }
+// }
+// case ParcelHistoryRetrieved(parcelHistory) =>
+// val parcelId = parcelHistory.id
+// val parcelState = state.parcelStates(parcelId)
+// val attributesChangedEvents: Seq[Event] = (if (parcelState.attributes.isEmpty)
+// Some(parcelHistory.attributes)
+// else
+// parcelState.attributes.flatMap(oldAttributes => if (oldAttributes != parcelHistory.attributes) Some(parcelHistory.attributes) else None))
+// .map(attributes => ParcelAttributesChanged(parcelId, attributes))
+// .toSeq
+
+// val newStates = parcelHistory.states.state.toSet -- parcelState.states
+// val stateEvents: Seq[Event] = newStates.map(state => ParcelHistoryStateAdded(parcelId, state)).toSeq
+// val comment = state.parcelStates(parcelId).comment
+
+// Effect
+// .persist(attributesChangedEvents ++ stateEvents)
+// .thenRun(_ => {
+// if (newStates.nonEmpty) {
+// stateReporter ! DeliveryStateChanged(Parcel(comment, None, newStates).fullStatePrint(parcelId))
+// }
+// })
+// }
+// }
+
+// val eventHandler: EventHandler[State, Event] = (state, evt) => {
+// evt match {
+// case ParcelAdded(parcelId, comment) =>
+// state.copy(parcelStates = state.parcelStates + (parcelId -> Parcel(comment)))
+// case ParcelRemoved(parcelId) => state.copy(parcelStates = state.parcelStates - parcelId)
+// case ParcelHistoryStateAdded(parcelId, newState) =>
+// val parcelState = state.parcelStates(parcelId)
+// val newParcelState = parcelState.copy(states = parcelState.states + newState)
+// state.copy(parcelStates = state.parcelStates.updated(parcelId, newParcelState))
+// case ParcelAttributesChanged(parcelId, newAttributes) =>
+// val parcelState = state.parcelStates(parcelId)
+// val newParcelState = parcelState.copy(attributes = Some(newAttributes))
+// state.copy(parcelStates = state.parcelStates.updated(parcelId, newParcelState))
+// }
+// }
+
+// EventSourcedBehavior[Command, Event, State](persistenceId = PersistenceId(entityType, chatId), emptyState = State(), commandHandler = commandHandler, eventHandler = eventHandler)
+// }
+// }
+}
diff --git a/src/main/scala/tech/xeppaka/bot/DialogManager.scala b/src/main/scala/tech/xeppaka/bot/DialogManager.scala
new file mode 100644
index 0000000..3f4a0c9
--- /dev/null
+++ b/src/main/scala/tech/xeppaka/bot/DialogManager.scala
@@ -0,0 +1,54 @@
+package eu.xeppaka.bot
+
+// import akka.actor.typed.scaladsl.Behaviors
+// import akka.actor.typed.{ActorRef, Behavior}
+// import akka.util.Timeout
+// import eu.xeppaka.bot.CheckDeliveryDialog.{ProcessMessageFailure, ProcessMessageSuccess}
+// import eu.xeppaka.telegram.bot.TelegramEntities._
+
+// import scala.concurrent.duration._
+// import scala.util.{Failure, Success}
+
+object DialogManager {
+ // sealed trait Command
+ // sealed trait CommandResult
+
+ // case class ProcessUpdate(update: Update, replyTo: ActorRef[CommandResult]) extends Command
+ // case object ProcessUpdateSuccess extends CommandResult
+ // case class ProcessUpdateFailure(exception: Throwable) extends CommandResult
+
+ // // internal messages
+ // private case class DialogResponseSuccess(dialogId: Long, replyTo: ActorRef[CommandResult]) extends Command
+ // private case class DialogResponseFailure(dialogId: Long, exception: Throwable, replyTo: ActorRef[CommandResult]) extends Command
+
+ // def behavior(botUri: BotUri): Behavior[Command] = Behaviors.setup[Command] { ctx =>
+ // Behaviors.receiveMessagePartial {
+ // case ProcessUpdate(update, replyTo) =>
+ // if (update.message.isDefined) {
+ // val chatId = update.message.get.chat.id
+ // ctx.log.debug("action=process_update chat_id={} message={}", chatId, update.message.get)
+ // val msg = update.message.get
+ // val dialogActor = ctx.child(chatId.toString).getOrElse(ctx.spawn(CheckDeliveryDialog.behavior(chatId, botUri), chatId.toString)).unsafeUpcast[CheckDeliveryDialog.Command]
+ // ctx.log.info("action=ask_dialog id={}", chatId)
+
+ // implicit val timeout: Timeout = 5.seconds
+ // ctx.ask[CheckDeliveryDialog.Command, CheckDeliveryDialog.CommandResult](dialogActor, replyTo => CheckDeliveryDialog.ProcessMessage(msg, replyTo)) {
+ // case Success(ProcessMessageSuccess) => DialogResponseSuccess(chatId, replyTo)
+ // case Success(ProcessMessageFailure(exception)) => DialogResponseFailure(chatId, exception, replyTo)
+ // case Failure(exception) => DialogResponseFailure(chatId, exception, replyTo)
+ // }
+ // } else {
+ // ctx.log.debug("action=process_update result=success message=update message is empty")
+ // }
+ // Behaviors.same
+ // case DialogResponseSuccess(dialogId, replyTo) =>
+ // ctx.log.info("action=ask_dialog id={} result=success", dialogId)
+ // replyTo ! ProcessUpdateSuccess
+ // Behaviors.same
+ // case DialogResponseFailure(dialogId, exception, replyTo) =>
+ // ctx.log.error(s"action=ask_dialog id=$dialogId result=failure", exception)
+ // replyTo ! ProcessUpdateFailure(exception)
+ // Behaviors.same
+ // }
+ // }
+}
diff --git a/src/main/scala/eu/xeppaka/bot/JsonSerializable.scala b/src/main/scala/tech/xeppaka/bot/JsonSerializable.scala
similarity index 100%
rename from src/main/scala/eu/xeppaka/bot/JsonSerializable.scala
rename to src/main/scala/tech/xeppaka/bot/JsonSerializable.scala
diff --git a/src/main/scala/tech/xeppaka/bot/Main.scala b/src/main/scala/tech/xeppaka/bot/Main.scala
new file mode 100644
index 0000000..230ae75
--- /dev/null
+++ b/src/main/scala/tech/xeppaka/bot/Main.scala
@@ -0,0 +1,50 @@
+package eu.xeppaka.bot
+
+// import java.nio.file.Paths
+// import akka.actor.Scheduler
+// import akka.actor.typed.scaladsl.AskPattern._
+// import akka.actor.typed.scaladsl.Behaviors
+// import akka.actor.typed.scaladsl.adapter._
+// import akka.actor.typed.{ActorSystem, DispatcherSelector, SupervisorStrategy}
+// import akka.http.scaladsl.Http
+// import akka.util.Timeout
+// import akka.{actor, Done}
+// import com.fasterxml.jackson.annotation.JsonInclude
+// import com.fasterxml.jackson.databind.DeserializationFeature
+// import de.heikoseeberger.akkahttpjackson.JacksonSupport
+
+// import scala.concurrent.duration._
+// import scala.concurrent.{Await, ExecutionContextExecutor, Future}
+// import scala.io.StdIn
+
+object Main {
+ // JacksonSupport.defaultObjectMapper.setSerializationInclusion(JsonInclude.Include.NON_EMPTY)
+ // JacksonSupport.defaultObjectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
+
+ def main(args: Array[String]): Unit = {
+ // val botId = System.getProperty("botId", "570855144:AAEv7b817cuq2JJI9f2kG5B9G3zW1x-btz4")
+ // val localPort = 8443
+ // val hookDomain = System.getProperty("hookDomain", "xeppaka.eu")
+ // val hookPort = System.getProperty("hookPort", "8443").toInt
+ // val useHttpsServer = System.getProperty("useHttpsServer", "true").toBoolean
+
+ // val botBehavior = Behaviors.supervise(TelegramBot.behavior(botId, "0.0.0.0", localPort, hookDomain, hookPort, useHttpsServer)).onFailure(SupervisorStrategy.restart)
+ // val telegramBot = ActorSystem(botBehavior, "telegram-bot-delivery")
+
+// implicit val actorSystem: actor.ActorSystem = telegramBot.toUntyped
+// implicit val executionContext: ExecutionContextExecutor = telegramBot.dispatchers.lookup(DispatcherSelector.default())
+// implicit val scheduler: Scheduler = telegramBot.scheduler
+// implicit val timeout: Timeout = 10.seconds
+
+// println("Press enter to finish bot...")
+// StdIn.readLine()
+//
+// val stopFuture: Future[Done] = telegramBot ? (ref => TelegramBot.Stop(ref))
+//
+// val terminateFuture = stopFuture
+// .andThen { case _ => Http().shutdownAllConnectionPools() }
+// .andThen { case _ => telegramBot.terminate() }
+//
+// Await.ready(terminateFuture, 20.seconds)
+ }
+}
diff --git a/src/main/scala/eu/xeppaka/bot/PostType.scala b/src/main/scala/tech/xeppaka/bot/PostType.scala
similarity index 100%
rename from src/main/scala/eu/xeppaka/bot/PostType.scala
rename to src/main/scala/tech/xeppaka/bot/PostType.scala
diff --git a/src/main/scala/tech/xeppaka/bot/TelegramBot.scala b/src/main/scala/tech/xeppaka/bot/TelegramBot.scala
new file mode 100644
index 0000000..dcc0da5
--- /dev/null
+++ b/src/main/scala/tech/xeppaka/bot/TelegramBot.scala
@@ -0,0 +1,252 @@
+package eu.xeppaka.bot
+
+import java.io.InputStream
+import java.security.{KeyStore, SecureRandom}
+import java.util.UUID
+
+// import akka.Done
+// import akka.actor.ActorSystem
+// import akka.actor.typed.scaladsl.Behaviors
+// import akka.actor.typed.scaladsl.adapter._
+// import akka.actor.typed._
+// import akka.http.scaladsl.marshalling.Marshal
+// import akka.http.scaladsl.model._
+// import akka.http.scaladsl.server.Directives.{as, complete, entity, extractLog, onComplete, path, post}
+// import akka.http.scaladsl.server.Route
+// import akka.http.scaladsl.{ConnectionContext, Http, HttpExt, HttpsConnectionContext}
+// import akka.util.{ByteString, Timeout}
+// import eu.xeppaka.telegram.bot.TelegramEntities._
+// import javax.net.ssl.{KeyManagerFactory, SSLContext, TrustManagerFactory}
+
+// import scala.collection.immutable
+// import scala.concurrent.ExecutionContextExecutor
+// import scala.concurrent.duration._
+// import scala.io.Source
+// import scala.util.{Failure, Success}
+
+object TelegramBot {
+// sealed trait Command
+// sealed trait CommandResult
+// sealed trait StopResult extends CommandResult
+
+// case class Stop(replyTo: ActorRef[Done]) extends Command
+// case object GetBotInfo
+// case object GetWebhookInfo
+
+// def behavior(botId: String, interface: String, localPort: Int, hookDomain: String, hookPort: Int, useHttpsServer: Boolean = true): Behavior[Command] = Behaviors.withStash(100) { stashBuffer =>
+// Behaviors.setup[Command] { ctx =>
+// ctx.log.info("action=start_bot")
+
+// implicit val untypedSystem: ActorSystem = ctx.system.toClassic
+// implicit val executionContextExecutor: ExecutionContextExecutor = ctx.system.dispatchers.lookup(DispatcherSelector.default())
+
+// val botUri = BotUri(botId)
+// val http: HttpExt = Http()
+// val hookId = UUID.randomUUID().toString
+// val webhookUri = Uri(s"https://$hookDomain:$hookPort/$hookId")
+// val dialogManager = ctx.spawnAnonymous(Behaviors.supervise(DialogManager.behavior(botUri)).onFailure(SupervisorStrategy.restart))
+// val routes = botRoutes(hookId, dialogManager)(ctx.system.scheduler)
+
+// def bindServer: Behavior[Command] = Behaviors.setup[Command] { ctx =>
+// case class BindingSuccess(binding: Http.ServerBinding) extends Command
+// case class BindingFailure(exception: Throwable) extends Command
+
+// ctx.log.info("action=bind_server interface={} port={}", interface, localPort)
+
+// val serverBuilder = http.newServerAt(interface, localPort)
+// val bindFuture = if (useHttpsServer) {
+// serverBuilder.enableHttps(createHttpsConnectionContext).bindFlow(routes)
+// } else {
+// serverBuilder.bindFlow(routes)
+// }
+
+// bindFuture.onComplete {
+// case Success(binding) => ctx.self ! BindingSuccess(binding)
+// case Failure(exception) => ctx.self ! BindingFailure(exception)
+// }
+
+// Behaviors.receiveMessage[Command] {
+// case BindingSuccess(binding) =>
+// ctx.log.info("action=bind_server result=success")
+// setWebhook(binding)
+// case BindingFailure(exception) =>
+// ctx.log.error("action=bind_server result=failure", exception)
+// ctx.log.error("action=start_bot result=failure")
+// Behaviors.stopped
+// case otherCommand: Command =>
+// stashBuffer.stash(otherCommand)
+// Behaviors.same
+// }
+// }
+
+// def unbindServer(binding: Http.ServerBinding, replyTo: Option[ActorRef[Done]]): Behavior[Command] = Behaviors.setup[Command] { ctx =>
+// case object UnbindingSuccess extends Command
+// case class UnbindingFailure(exception: Throwable) extends Command
+
+// ctx.log.info("action=unbind_server interface={} port={}", interface, localPort)
+
+// binding.unbind().onComplete {
+// case Success(Done) => ctx.self ! UnbindingSuccess
+// case Failure(exception) => ctx.self ! UnbindingFailure(exception)
+// }
+
+// Behaviors.receiveMessage[Command] {
+// case UnbindingSuccess =>
+// ctx.log.info("action=unbind_server result=success")
+// replyTo.foreach(_ ! Done)
+// Behaviors.stopped
+// case UnbindingFailure(exception) =>
+// ctx.log.error("action=unbind_server result=failure", exception)
+// replyTo.foreach(_ ! Done)
+// Behaviors.stopped
+// case _ => Behaviors.unhandled
+// }
+// }
+
+// def setWebhook(binding: Http.ServerBinding, attempt: Int = 1): Behavior[Command] = Behaviors.setup[Command] { ctx =>
+// case object SetWebhookSuccess extends Command
+// case class SetWebhookFailure(exception: Throwable) extends Command
+
+// ctx.log.info("action=set_webhook url={} webhook={}", botUri.setWebhook, webhookUri)
+
+// implicit val executionContextExecutor: ExecutionContextExecutor = ctx.system.dispatchers.lookup(DispatcherSelector.default())
+
+// val urlEntity = HttpEntity.Strict(ContentTypes.`text/plain(UTF-8)`, ByteString(webhookUri.toString()))
+// val urlPart = Some(Multipart.FormData.BodyPart.Strict("url", urlEntity))
+
+// val certificatePart = if (useHttpsServer) {
+// val certificate = ByteString(Source.fromResource("telegram-bot.pem").mkString)
+// val certificateEntity = HttpEntity.Strict(ContentTypes.`application/octet-stream`, certificate)
+
+// Some(Multipart.FormData.BodyPart.Strict("certificate", certificateEntity, Map("filename" -> "cert.pem")))
+// } else {
+// None
+// }
+
+// val formParts = immutable.Seq(urlPart, certificatePart).flatten
+// val formData = Multipart.FormData.Strict(formParts)
+
+// Marshal(formData).to[RequestEntity].flatMap(requestEntity => http.singleRequest(HttpRequest(uri = botUri.setWebhook, method = HttpMethods.POST, entity = requestEntity))).onComplete {
+// case Success(response) =>
+// if (response.status.isSuccess())
+// ctx.self ! SetWebhookSuccess
+// else
+// ctx.self ! SetWebhookFailure(new RuntimeException(s"Set webhook HTTP response status: ${response.status.value}."))
+// case Failure(exception) =>
+// ctx.self ! SetWebhookFailure(exception)
+// }
+
+// Behaviors.receiveMessage {
+// case SetWebhookSuccess =>
+// ctx.log.info("action=set_webhook result=success")
+// stashBuffer.unstashAll(started(binding))
+// case SetWebhookFailure(exception) =>
+// if (attempt > 20) {
+// ctx.log.error(s"action=set_webhook result=failure attempt=$attempt", exception)
+// ctx.log.error("action=start_bot result=failure")
+// unbindServer(binding, None)
+// } else {
+// setWebhook(binding, attempt = attempt + 1)
+// }
+// case otherCommand: Command =>
+// stashBuffer.stash(otherCommand)
+// Behaviors.same
+// }
+// }
+
+// def deletingWebhook(binding: Http.ServerBinding, replyTo: ActorRef[Done]): Behavior[Command] = Behaviors.setup[Command] { ctx =>
+// case object DeleteWebhookSuccess extends Command
+// case class DeleteWebhookFailure(exception: Throwable) extends Command
+
+// ctx.log.info("action=delete_webhook url={} webhook={}", botUri.deleteWebhook, webhookUri)
+
+// implicit val executionContextExecutor: ExecutionContextExecutor = ctx.system.dispatchers.lookup(DispatcherSelector.default())
+
+// http.singleRequest(HttpRequest(uri = botUri.deleteWebhook, method = HttpMethods.POST)).onComplete {
+// case Success(response) =>
+// if (response.status.isSuccess())
+// ctx.self ! DeleteWebhookSuccess
+// else
+// ctx.self ! DeleteWebhookFailure(new RuntimeException(s"Delete webhook HTTP response status: ${response.status.value}"))
+// case Failure(exception) =>
+// ctx.self ! DeleteWebhookFailure(exception)
+// }
+
+// Behaviors.receiveMessage {
+// case DeleteWebhookSuccess =>
+// ctx.log.info("action=delete_webhook result=success")
+// unbindServer(binding, Some(replyTo))
+// case DeleteWebhookFailure(exception) =>
+// ctx.log.error("action=delete_webhook result=failure", exception)
+// unbindServer(binding, Some(replyTo))
+// case _ => Behaviors.unhandled
+// }
+// }
+
+// def started(binding: Http.ServerBinding): Behavior[Command] = Behaviors.setup[Command] { ctx =>
+// ctx.log.info("action=start_bot result=success")
+
+// Behaviors.receiveMessage[Command] {
+// case Stop(replyTo) =>
+// ctx.log.info("action=stop_bot")
+// deletingWebhook(binding, replyTo)
+// case _ =>
+// Behaviors.unhandled
+// }
+// }
+
+// bindServer
+// }
+// }
+
+// private def botRoutes(hookId: String, updatesProcessor: ActorRef[DialogManager.ProcessUpdate])(implicit scheduler: Scheduler): Route = {
+// import de.heikoseeberger.akkahttpjackson.JacksonSupport._
+// import akka.actor.typed.scaladsl.AskPattern._
+
+// implicit val timeout: Timeout = 30.seconds
+
+// path(hookId) {
+// post {
+// extractLog { log =>
+// entity(as[Update]) { update =>
+// // log.info("update={}", update)
+// // complete(StatusCodes.OK)
+// onComplete(updatesProcessor.ask[DialogManager.CommandResult](ref => DialogManager.ProcessUpdate(update, ref))) {
+// case Success(processResult) =>
+// processResult match {
+// case DialogManager.ProcessUpdateSuccess => complete(HttpResponse(status = StatusCodes.OK))
+// case DialogManager.ProcessUpdateFailure(exception) =>
+// log.error(exception, "action=process_update result=failure message={}", update)
+// complete(HttpResponse(status = StatusCodes.InternalServerError))
+// }
+// case Failure(exception) =>
+// log.error(exception, "action=process_update result=failure message={}", update)
+// complete(HttpResponse(status = StatusCodes.InternalServerError))
+// }
+// }
+// }
+// }
+// }
+// }
+
+// private def createHttpsConnectionContext: HttpsConnectionContext = {
+// val password: Array[Char] = "".toCharArray // do not store passwords in code, read them from somewhere safe!
+
+// val ks: KeyStore = KeyStore.getInstance("PKCS12")
+// val keystore: InputStream = getClass.getResourceAsStream("/telegram-bot.p12")
+
+// require(keystore != null, "Keystore required!")
+// ks.load(keystore, password)
+
+// val keyManagerFactory: KeyManagerFactory = KeyManagerFactory.getInstance("SunX509")
+// keyManagerFactory.init(ks, password)
+
+// val tmf: TrustManagerFactory = TrustManagerFactory.getInstance("SunX509")
+// tmf.init(ks)
+
+// val sslContext: SSLContext = SSLContext.getInstance("TLS")
+// sslContext.init(keyManagerFactory.getKeyManagers, tmf.getTrustManagers, new SecureRandom)
+
+// ConnectionContext.https(sslContext)
+// }
+}
diff --git a/src/main/scala/tech/xeppaka/bot/cats/Delivery.scala b/src/main/scala/tech/xeppaka/bot/cats/Delivery.scala
new file mode 100644
index 0000000..b3b97fc
--- /dev/null
+++ b/src/main/scala/tech/xeppaka/bot/cats/Delivery.scala
@@ -0,0 +1,5 @@
+package tech.xeppaka.bot.cats
+
+trait Delivery {
+ def states: Seq[String]
+}
diff --git a/src/main/scala/tech/xeppaka/bot/cats/Dialog.scala b/src/main/scala/tech/xeppaka/bot/cats/Dialog.scala
new file mode 100644
index 0000000..7793159
--- /dev/null
+++ b/src/main/scala/tech/xeppaka/bot/cats/Dialog.scala
@@ -0,0 +1,12 @@
+package tech.xeppaka.bot.cats
+
+object Dialog {
+ trait DialogStep {
+ def enterText: Option[String]
+ def exitText: Option[String]
+ }
+}
+
+trait Dialog {
+ def
+}
diff --git a/src/main/scala/tech/xeppaka/bot/cats/DialogDelivery.scala b/src/main/scala/tech/xeppaka/bot/cats/DialogDelivery.scala
new file mode 100644
index 0000000..d0e58b9
--- /dev/null
+++ b/src/main/scala/tech/xeppaka/bot/cats/DialogDelivery.scala
@@ -0,0 +1,36 @@
+package tech.xeppaka.bot.cats
+
+import cats.data.Validated
+import cats.data.State
+import tech.xeppaka.bot.cats.Dialog.DialogStep
+
+object DialogDelivery {
+ object Command {
+ def validateCommand(command: String): Validated[Errors.Error, Command] = {
+ command match {
+ case "/add" => Validated.Valid(Command.AddDelivery)
+ case "/remove" => Validated.Valid(Command.RemoveDelivery)
+ case _ => Validated.Invalid(Errors.InvalidCommand(command))
+ }
+ }
+ }
+
+ enum Command {
+ case AddDelivery, RemoveDelivery
+ }
+
+ case class InitialDialogStep() extends DialogStep {
+ def enterText: Option[String] = Some("Please enter command.")
+ def exitText: Option[String] = None
+ }
+
+ case class AddDeliveryDialogStep() extends DialogStep {
+ def enterText: Option[String] = Some("Please enter id.")
+ def exitText: Option[String] = None
+ }
+
+ case class RemoveDeliveryDialogStep() extends DialogStep {
+ def enterText: Option[String] = Some("Please enter id.")
+ def exitText: Option[String] = None
+ }
+}
diff --git a/src/main/scala/tech/xeppaka/bot/cats/DialogStep.scala b/src/main/scala/tech/xeppaka/bot/cats/DialogStep.scala
new file mode 100644
index 0000000..a518323
--- /dev/null
+++ b/src/main/scala/tech/xeppaka/bot/cats/DialogStep.scala
@@ -0,0 +1,2 @@
+package tech.xeppaka.bot.cats
+
diff --git a/src/main/scala/tech/xeppaka/bot/cats/Errors.scala b/src/main/scala/tech/xeppaka/bot/cats/Errors.scala
new file mode 100644
index 0000000..c417cd4
--- /dev/null
+++ b/src/main/scala/tech/xeppaka/bot/cats/Errors.scala
@@ -0,0 +1,7 @@
+package tech.xeppaka.bot.cats
+
+object Errors {
+ sealed trait Error
+ case class InvalidCommand(command: String) extends Error
+ case class InvalidText(text: String) extends Error
+}
diff --git a/src/main/scala/tech/xeppaka/bot/cats/IdDelivery.scala b/src/main/scala/tech/xeppaka/bot/cats/IdDelivery.scala
new file mode 100644
index 0000000..c2e11fc
--- /dev/null
+++ b/src/main/scala/tech/xeppaka/bot/cats/IdDelivery.scala
@@ -0,0 +1,5 @@
+package tech.xeppaka.bot.cats
+
+final case class IdDelivery(id: String) extends AnyVal {
+ override def toString(): String = id
+}
diff --git a/src/main/scala/tech/xeppaka/bot/cats/TelegramBot.scala b/src/main/scala/tech/xeppaka/bot/cats/TelegramBot.scala
new file mode 100644
index 0000000..21aee78
--- /dev/null
+++ b/src/main/scala/tech/xeppaka/bot/cats/TelegramBot.scala
@@ -0,0 +1,14 @@
+package tech.xeppaka.bot.cats
+
+import cats.effect.IOApp
+import cats.effect.IO
+
+object TelegramBot extends IOApp.Simple {
+ val botId = System.getProperty("botId", "570855144:AAEv7b817cuq2JJI9f2kG5B9G3zW1x-btz4")
+
+ def run: IO[Unit] = {
+ val command = "/add1"
+ BotCommand.validateCommand(command)
+ IO.unit
+ }
+}
diff --git a/src/main/scala/tech/xeppaka/bot/cats/User.scala b/src/main/scala/tech/xeppaka/bot/cats/User.scala
new file mode 100644
index 0000000..6a43a37
--- /dev/null
+++ b/src/main/scala/tech/xeppaka/bot/cats/User.scala
@@ -0,0 +1,3 @@
+package tech.xeppaka.bot.cats
+
+final case class User(deliveries: Map[IdDelivery, Delivery] = Map.empty, dialogStep: Option[DialogStep])
diff --git a/src/main/scala/tech/xeppaka/bot/cats/Validation.scala b/src/main/scala/tech/xeppaka/bot/cats/Validation.scala
new file mode 100644
index 0000000..b0cca9a
--- /dev/null
+++ b/src/main/scala/tech/xeppaka/bot/cats/Validation.scala
@@ -0,0 +1,11 @@
+package tech.xeppaka.bot.cats
+
+import cats.data.ValidatedNel
+
+object Validation {
+ type ValidationResult[A] = ValidatedNel[ValidationFailure, A]
+
+ trait ValidationFailure {
+ def errorMessage: String
+ }
+}