Upgrade to scala 2.13.1 and akka 2.6.3

This commit is contained in:
Pavel Kachalouski
2020-03-11 22:21:53 +01:00
parent 494bf71e61
commit 90cce667e6
8 changed files with 481 additions and 516 deletions

View File

@@ -13,12 +13,13 @@ lazy val `telegram-bot-delivery` = (project in file("."))
name := "telegram-bot-delivery", name := "telegram-bot-delivery",
libraryDependencies ++= Seq( libraryDependencies ++= Seq(
scalaTest % Test, scalaTest % Test,
akka,
akkaTyped, akkaTyped,
akkaClusterShardingTyped,
akkaHttp, akkaHttp,
akkaStream, akkaStream,
akkaPersistence, akkaPersistence,
akkaPersistenceCassandra, akkaPersistenceCassandra,
akkaPersistenceQuery,
levelDbJni, levelDbJni,
circleCore, circleCore,
circleGeneric, circleGeneric,
@@ -26,13 +27,13 @@ lazy val `telegram-bot-delivery` = (project in file("."))
circeAkkaHttp, circeAkkaHttp,
slibTelegram slibTelegram
), ),
dockerBaseImage := "openjdk:13-jdk-oracle", dockerBaseImage := "openjdk:11",
dockerExposedPorts := Seq(8443), dockerExposedPorts := Seq(8443),
dockerRepository := Some("registry.xeppaka.eu:443"), dockerRepository := Some("registry.xeppaka.eu:443"),
Docker / daemonUserUid := Some("1001"), Docker / daemonUserUid := Some("1001"),
Docker / daemonUser := "telegram-bot", Docker / daemonUser := "telegram-bot",
Docker / defaultLinuxInstallLocation := "/opt/telegram-bot-delivery", Docker / defaultLinuxInstallLocation := "/opt/telegram-bot-delivery",
version := "1.0.1" version := "1.1.1"
) )
.enablePlugins(JavaServerAppPackaging) .enablePlugins(JavaServerAppPackaging)
.enablePlugins(DockerPlugin) .enablePlugins(DockerPlugin)

View File

@@ -4,22 +4,23 @@ import Dependencies.Versions._
object Dependencies { object Dependencies {
object Versions { object Versions {
val akkaVersion = "2.5.26" val akkaVersion = "2.6.3"
val akkaHttpVersion = "10.1.10" val akkaHttpVersion = "10.1.11"
val akkaPersistenceCassandraVersion = "0.100" val akkaPersistenceCassandraVersion = "0.103"
val levelDbJniVersion = "1.8" val levelDbJniVersion = "1.8"
val circeVersion = "0.12.3" val circeVersion = "0.13.0"
val akkaHttpCirceVersion = "1.29.1" val akkaHttpCirceVersion = "1.31.0"
val scalaTestVersion = "3.2.0-M1" val scalaTestVersion = "3.2.0-M4"
val slibTelegramVersion = "0.1.0" val slibTelegramVersion = "0.1.0"
} }
val akka = "com.typesafe.akka" %% "akka-actor" % akkaVersion
val akkaTyped = "com.typesafe.akka" %% "akka-actor-typed" % akkaVersion val akkaTyped = "com.typesafe.akka" %% "akka-actor-typed" % akkaVersion
val akkaStream = "com.typesafe.akka" %% "akka-stream" % akkaVersion val akkaStream = "com.typesafe.akka" %% "akka-stream" % akkaVersion
val akkaHttp = "com.typesafe.akka" %% "akka-http" % akkaHttpVersion val akkaHttp = "com.typesafe.akka" %% "akka-http" % akkaHttpVersion
val akkaPersistence = "com.typesafe.akka" %% "akka-persistence-typed" % akkaVersion val akkaPersistence = "com.typesafe.akka" %% "akka-persistence-typed" % akkaVersion
val akkaPersistenceCassandra = "com.typesafe.akka" %% "akka-persistence-cassandra" % "0.100" 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 levelDbJni = "org.fusesource.leveldbjni" % "leveldbjni-all" % levelDbJniVersion val levelDbJni = "org.fusesource.leveldbjni" % "leveldbjni-all" % levelDbJniVersion
val circleCore = "io.circe" %% "circe-core" % circeVersion val circleCore = "io.circe" %% "circe-core" % circeVersion
val circleGeneric = "io.circe" %% "circe-generic" % circeVersion val circleGeneric = "io.circe" %% "circe-generic" % circeVersion

View File

@@ -1 +1 @@
sbt.version=1.3.3 sbt.version=1.3.8

View File

@@ -1,2 +1,3 @@
addSbtPlugin("com.typesafe.sbt" % "sbt-native-packager" % "1.4.1") addSbtPlugin("com.typesafe.sbt" % "sbt-native-packager" % "1.4.1")
addSbtPlugin("com.github.gseitz" % "sbt-release" % "1.0.12") addSbtPlugin("com.github.gseitz" % "sbt-release" % "1.0.12")
addSbtPlugin("net.virtual-void" % "sbt-dependency-graph" % "0.10.0-RC1")

View File

@@ -1,11 +1,11 @@
package eu.xeppaka.bot package eu.xeppaka.bot
import akka.actor.ActorSystem
import akka.actor.typed.scaladsl.Behaviors
import akka.actor.typed.scaladsl.adapter._ import akka.actor.typed.scaladsl.adapter._
import akka.actor.typed.scaladsl.{Behaviors, StashBuffer} import akka.actor.typed.{ActorRef, Behavior, SupervisorStrategy}
import akka.actor.typed.{ActorRef, Behavior, DispatcherSelector, SupervisorStrategy}
import akka.http.scaladsl.Http import akka.http.scaladsl.Http
import akka.http.scaladsl.model._ import akka.http.scaladsl.model._
import akka.stream.ActorMaterializer
import akka.stream.scaladsl.{Sink, Source} import akka.stream.scaladsl.{Sink, Source}
import akka.util.{ByteString, Timeout} import akka.util.{ByteString, Timeout}
import eu.xeppaka.telegram.bot.TelegramEntities._ import eu.xeppaka.telegram.bot.TelegramEntities._
@@ -59,10 +59,9 @@ object CheckDeliveryDialog {
private val removeKeyboard = Some(ReplyKeyboardRemove()) private val removeKeyboard = Some(ReplyKeyboardRemove())
def behavior(chatId: Long, botUri: BotUri): Behavior[Command] = Behaviors.setup[Command] { ctx => def behavior(chatId: Long, botUri: BotUri): Behavior[Command] = Behaviors.setup[Command] { ctx =>
implicit val materializer: ActorMaterializer = ActorMaterializer()(ctx.system.toClassic) implicit val system: ActorSystem = ctx.system.toClassic
implicit val executionContext: ExecutionContext = ctx.system.dispatchers.lookup(DispatcherSelector.default()) implicit val executionContext: ExecutionContext = ctx.executionContext
val http = Http()(ctx.system.toClassic) val http = Http()
val stashBuffer = StashBuffer[Command](100)
val deliveryStateAdapter: ActorRef[CzechPostDeliveryCheck.DeliveryStateChanged] = ctx.messageAdapter(stateChanged => DeliveryStateChanged(stateChanged.state)) 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)) val czechPostDeliveryCheck = ctx.spawnAnonymous(Behaviors.supervise(CzechPostDeliveryCheck.behavior(chatId.toString, deliveryStateAdapter)).onFailure(SupervisorStrategy.restart))
@@ -96,12 +95,13 @@ object CheckDeliveryDialog {
Behaviors.unhandled Behaviors.unhandled
} }
def addParcel(parcelId: String, comment: String): Behavior[Command] = Behaviors.setup { ctx => def addParcel(parcelId: String, comment: String): Behavior[Command] = Behaviors.withStash(100) { stashBuffer =>
Behaviors.setup { ctx =>
case object AddParcelSuccess extends Command case object AddParcelSuccess extends Command
case class AddParcelFailure(exception: Throwable) extends Command case class AddParcelFailure(exception: Throwable) extends Command
implicit val timeout: Timeout = 5.seconds implicit val timeout: Timeout = 5.seconds
ctx.ask[CzechPostDeliveryCheck.Command, CzechPostDeliveryCheck.CommandResult](czechPostDeliveryCheck)(ref => CzechPostDeliveryCheck.AddParcel(parcelId, comment, ref)) { ctx.ask[CzechPostDeliveryCheck.Command, CzechPostDeliveryCheck.CommandResult](czechPostDeliveryCheck, ref => CzechPostDeliveryCheck.AddParcel(parcelId, comment, ref)) {
case Success(CzechPostDeliveryCheck.CommandResultSuccess) => AddParcelSuccess case Success(CzechPostDeliveryCheck.CommandResultSuccess) => AddParcelSuccess
case Success(CzechPostDeliveryCheck.CommandResultFailure(exception)) => AddParcelFailure(exception) case Success(CzechPostDeliveryCheck.CommandResultFailure(exception)) => AddParcelFailure(exception)
case Failure(exception) => AddParcelFailure(exception) case Failure(exception) => AddParcelFailure(exception)
@@ -117,7 +117,7 @@ object CheckDeliveryDialog {
val message = SendMessage(chatId, s"Parcel $parcelId is in the watch list already.", reply_markup = commandsKeyboard) val message = SendMessage(chatId, s"Parcel $parcelId is in the watch list already.", reply_markup = commandsKeyboard)
sendMessage(message, waitCommand, waitCommand) sendMessage(message, waitCommand, waitCommand)
case _ => case _ =>
ctx.log.error(exception, "action=add_parcel result=failure") ctx.log.error("action=add_parcel result=failure", exception)
val message = SendMessage(chatId, s"Adding parcel failed. Please try again.", reply_markup = commandsKeyboard) val message = SendMessage(chatId, s"Adding parcel failed. Please try again.", reply_markup = commandsKeyboard)
sendMessage(message, waitCommand, waitCommand) sendMessage(message, waitCommand, waitCommand)
} }
@@ -126,13 +126,15 @@ object CheckDeliveryDialog {
Behaviors.same Behaviors.same
} }
} }
}
def listParcels: Behavior[Command] = Behaviors.setup { ctx => def listParcels: Behavior[Command] = Behaviors.withStash(100) { stashBuffer =>
Behaviors.setup { ctx =>
case class ListParcelsSuccess(parcelsList: Seq[String]) extends Command case class ListParcelsSuccess(parcelsList: Seq[String]) extends Command
case class ListParcelsFailure(exception: Throwable) extends Command case class ListParcelsFailure(exception: Throwable) extends Command
implicit val timeout: Timeout = 5.seconds implicit val timeout: Timeout = 5.seconds
ctx.ask[CzechPostDeliveryCheck.Command, CzechPostDeliveryCheck.ListParcelsResult](czechPostDeliveryCheck)(ref => CzechPostDeliveryCheck.ListParcels(ref)) { ctx.ask[CzechPostDeliveryCheck.Command, CzechPostDeliveryCheck.ListParcelsResult](czechPostDeliveryCheck, ref => CzechPostDeliveryCheck.ListParcels(ref)) {
case Success(CzechPostDeliveryCheck.ListParcelsResult(parcelsList)) => ListParcelsSuccess(parcelsList) case Success(CzechPostDeliveryCheck.ListParcelsResult(parcelsList)) => ListParcelsSuccess(parcelsList)
case Failure(exception) => ListParcelsFailure(exception) case Failure(exception) => ListParcelsFailure(exception)
} }
@@ -143,7 +145,7 @@ object CheckDeliveryDialog {
val message = SendMessage(chatId, messageText, Some("Markdown"), reply_markup = commandsKeyboard) val message = SendMessage(chatId, messageText, Some("Markdown"), reply_markup = commandsKeyboard)
sendMessage(message, waitCommand, waitCommand) sendMessage(message, waitCommand, waitCommand)
case ListParcelsFailure(exception) => case ListParcelsFailure(exception) =>
ctx.log.error(exception, "action=list_parcels result=failure chat_id={}", chatId) 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) val message = SendMessage(chatId, "Failed to get a list of your watched parcels. Please try again later.", reply_markup = commandsKeyboard)
sendMessage(message, waitCommand, waitCommand) sendMessage(message, waitCommand, waitCommand)
case otherMessage => case otherMessage =>
@@ -151,14 +153,15 @@ object CheckDeliveryDialog {
Behaviors.same Behaviors.same
} }
} }
}
def removeParcel(onSuccess: => Behavior[Command], onFailure: => Behavior[Command]): Behavior[Command] = def removeParcel(onSuccess: => Behavior[Command], onFailure: => Behavior[Command]): Behavior[Command] = Behaviors.withStash(100) { stashBuffer =>
Behaviors.setup { ctx => Behaviors.setup { ctx =>
case class ListParcelIdsSuccess(parcelsList: Seq[String]) extends Command case class ListParcelIdsSuccess(parcelsList: Seq[String]) extends Command
case class ListParcelIdsFailure(exception: Throwable) extends Command case class ListParcelIdsFailure(exception: Throwable) extends Command
implicit val timeout: Timeout = 5.seconds implicit val timeout: Timeout = 5.seconds
ctx.ask[CzechPostDeliveryCheck.Command, CzechPostDeliveryCheck.ListParcelIdsResult](czechPostDeliveryCheck)(ref => CzechPostDeliveryCheck.ListParcelIds(ref)) { ctx.ask[CzechPostDeliveryCheck.Command, CzechPostDeliveryCheck.ListParcelIdsResult](czechPostDeliveryCheck, ref => CzechPostDeliveryCheck.ListParcelIds(ref)) {
case Success(CzechPostDeliveryCheck.ListParcelIdsResult(parcelsList)) => ListParcelIdsSuccess(parcelsList) case Success(CzechPostDeliveryCheck.ListParcelIdsResult(parcelsList)) => ListParcelIdsSuccess(parcelsList)
case Failure(exception) => ListParcelIdsFailure(exception) case Failure(exception) => ListParcelIdsFailure(exception)
} }
@@ -175,7 +178,7 @@ object CheckDeliveryDialog {
sendMessage(message, onSuccess, onFailure) sendMessage(message, onSuccess, onFailure)
} }
case ListParcelIdsFailure(exception) => case ListParcelIdsFailure(exception) =>
ctx.log.error(exception, "action=list_parcels result=failure chat_id={}", chatId) 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) val message = SendMessage(chatId, "Failed to get a list of your watched parcels. Please try again later.", reply_markup = commandsKeyboard)
sendMessage(message, waitCommand, waitCommand) sendMessage(message, waitCommand, waitCommand)
case otherMessage => case otherMessage =>
@@ -183,13 +186,15 @@ object CheckDeliveryDialog {
Behaviors.same Behaviors.same
} }
} }
}
def removeParcelId(parcelId: String): Behavior[Command] = Behaviors.setup { ctx => def removeParcelId(parcelId: String): Behavior[Command] = Behaviors.withStash(100) { stashBuffer =>
Behaviors.setup { ctx =>
case object RemoveParcelSuccess extends Command case object RemoveParcelSuccess extends Command
case class RemoveParcelFailure(exception: Throwable) extends Command case class RemoveParcelFailure(exception: Throwable) extends Command
implicit val timeout: Timeout = 5.seconds implicit val timeout: Timeout = 5.seconds
ctx.ask[CzechPostDeliveryCheck.Command, CzechPostDeliveryCheck.CommandResult](czechPostDeliveryCheck)(ref => CzechPostDeliveryCheck.RemoveParcel(parcelId, ref)) { ctx.ask[CzechPostDeliveryCheck.Command, CzechPostDeliveryCheck.CommandResult](czechPostDeliveryCheck, ref => CzechPostDeliveryCheck.RemoveParcel(parcelId, ref)) {
case Success(CzechPostDeliveryCheck.CommandResultSuccess) => RemoveParcelSuccess case Success(CzechPostDeliveryCheck.CommandResultSuccess) => RemoveParcelSuccess
case Success(CzechPostDeliveryCheck.CommandResultFailure(exception)) => RemoveParcelFailure(exception) case Success(CzechPostDeliveryCheck.CommandResultFailure(exception)) => RemoveParcelFailure(exception)
case Failure(exception) => RemoveParcelFailure(exception) case Failure(exception) => RemoveParcelFailure(exception)
@@ -205,7 +210,7 @@ object CheckDeliveryDialog {
val message = SendMessage(chatId, s"Parcel $parcelId is not found in the list of the watched parcels.", reply_markup = commandsKeyboard) val message = SendMessage(chatId, s"Parcel $parcelId is not found in the list of the watched parcels.", reply_markup = commandsKeyboard)
sendMessage(message, waitCommand, waitCommand) sendMessage(message, waitCommand, waitCommand)
case _ => case _ =>
ctx.log.error(exception, "action=add_parcel result=failure") 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) val message = SendMessage(chatId, s"Remove of the parcel failed. Please try again.", reply_markup = commandsKeyboard)
sendMessage(message, waitCommand, waitCommand) sendMessage(message, waitCommand, waitCommand)
} }
@@ -214,6 +219,7 @@ object CheckDeliveryDialog {
Behaviors.same Behaviors.same
} }
} }
}
// def selectPostType(onFinish: PostType => Behavior[Command]): Behavior[Command] = Behaviors.receiveMessage { // def selectPostType(onFinish: PostType => Behavior[Command]): Behavior[Command] = Behaviors.receiveMessage {
// //
@@ -225,7 +231,8 @@ object CheckDeliveryDialog {
// sendMessage(message, waitParcelId(parcelId => addParcel(parcelId)), waitCommand) // sendMessage(message, waitParcelId(parcelId => addParcel(parcelId)), waitCommand)
// } // }
def waitTextMessage(onFinish: String => Behavior[Command]): Behavior[Command] = Behaviors.receiveMessage { def waitTextMessage(onFinish: String => Behavior[Command]): Behavior[Command] = Behaviors.withStash(100) { stashBuffer =>
Behaviors.receiveMessage {
case ProcessMessage(msg, replyTo) => case ProcessMessage(msg, replyTo) =>
if (msg.text.isDefined) { if (msg.text.isDefined) {
val parcelId = msg.text.get val parcelId = msg.text.get
@@ -239,8 +246,10 @@ object CheckDeliveryDialog {
stashBuffer.stash(otherMsg) stashBuffer.stash(otherMsg)
Behaviors.same Behaviors.same
} }
}
def sendMessage(message: SendMessage, onSuccess: => Behavior[Command], onFailure: => Behavior[Command], attempt: Int = 1): Behavior[Command] = Behaviors.setup[Command] { ctx => def sendMessage(message: SendMessage, onSuccess: => Behavior[Command], onFailure: => Behavior[Command], attempt: Int = 1): Behavior[Command] = Behaviors.withStash(100) { stashBuffer =>
Behaviors.setup[Command] { ctx =>
import io.circe.generic.auto._ import io.circe.generic.auto._
import io.circe.syntax._ import io.circe.syntax._
@@ -266,7 +275,7 @@ object CheckDeliveryDialog {
Success(SendMessageFailure(new RuntimeException(s"Error while sending message. HTTP status: ${response.status}."))) Success(SendMessageFailure(new RuntimeException(s"Error while sending message. HTTP status: ${response.status}.")))
} }
case Failure(exception) => case Failure(exception) =>
ctx.log.error(exception, "action=send_message status=finished result=failure chat_id={}", chatId) ctx.log.error(s"action=send_message status=finished result=failure chat_id=$chatId", exception)
Success(SendMessageFailure(exception)) Success(SendMessageFailure(exception))
} }
} }
@@ -276,13 +285,13 @@ object CheckDeliveryDialog {
Behaviors.receiveMessage { Behaviors.receiveMessage {
case SendMessageSuccess => case SendMessageSuccess =>
ctx.log.debug("action=send_message status=finished result=success chat_id={}", chatId) ctx.log.debug("action=send_message status=finished result=success chat_id={}", chatId)
stashBuffer.unstashAll(ctx, onSuccess) stashBuffer.unstashAll(onSuccess)
case SendMessageFailure(exception) => case SendMessageFailure(exception) =>
ctx.log.error(exception, "action=send_message status=finished result=failure chat_id={} attempt={}", chatId, attempt) ctx.log.error(s"action=send_message status=finished result=failure chat_id=$chatId attempt=$attempt", exception)
if (attempt > 5) { if (attempt > 5) {
ctx.log.error(exception, "action=send_message result=failure message=attempts threshold exceeded") ctx.log.error("action=send_message result=failure message=attempts threshold exceeded", exception)
stashBuffer.unstashAll(ctx, onFailure) stashBuffer.unstashAll(onFailure)
} else { } else {
sendMessage(message, onSuccess, onFailure, attempt + 1) sendMessage(message, onSuccess, onFailure, attempt + 1)
} }
@@ -291,6 +300,7 @@ object CheckDeliveryDialog {
Behaviors.same Behaviors.same
} }
} }
}
waitCommand waitCommand
} }

View File

@@ -4,8 +4,8 @@ import java.security.cert.X509Certificate
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import akka.actor.ActorSystem import akka.actor.ActorSystem
import akka.actor.typed.scaladsl.Behaviors
import akka.actor.typed.scaladsl.adapter._ import akka.actor.typed.scaladsl.adapter._
import akka.actor.typed.scaladsl.{Behaviors, TimerScheduler}
import akka.actor.typed.{ActorRef, Behavior, DispatcherSelector} import akka.actor.typed.{ActorRef, Behavior, DispatcherSelector}
import akka.http.scaladsl.UseHttp2.Negotiated import akka.http.scaladsl.UseHttp2.Negotiated
import akka.http.scaladsl.model._ import akka.http.scaladsl.model._
@@ -16,7 +16,6 @@ import akka.http.scaladsl.{Http, HttpsConnectionContext}
import akka.persistence.typed.PersistenceId import akka.persistence.typed.PersistenceId
import akka.persistence.typed.scaladsl.EventSourcedBehavior.{CommandHandler, EventHandler} import akka.persistence.typed.scaladsl.EventSourcedBehavior.{CommandHandler, EventHandler}
import akka.persistence.typed.scaladsl.{Effect, EventSourcedBehavior} import akka.persistence.typed.scaladsl.{Effect, EventSourcedBehavior}
import akka.stream.ActorMaterializer
import com.typesafe.sslconfig.akka.AkkaSSLConfig import com.typesafe.sslconfig.akka.AkkaSSLConfig
import de.heikoseeberger.akkahttpcirce.FailFastCirceSupport._ import de.heikoseeberger.akkahttpcirce.FailFastCirceSupport._
import io.circe.generic.auto._ import io.circe.generic.auto._
@@ -56,6 +55,7 @@ object Entities {
object CzechPostDeliveryCheck { object CzechPostDeliveryCheck {
private val czechPostDateFormat = new SimpleDateFormat("yyyy-MM-dd") private val czechPostDateFormat = new SimpleDateFormat("yyyy-MM-dd")
private val printDateFormat = new SimpleDateFormat("dd-MM-yyyy") private val printDateFormat = new SimpleDateFormat("dd-MM-yyyy")
private val entityType = "czechpost"
sealed trait Command sealed trait Command
sealed trait CommandResult sealed trait CommandResult
@@ -123,14 +123,12 @@ object CzechPostDeliveryCheck {
context context
} }
def behavior(chatId: String, stateReporter: ActorRef[DeliveryStateChanged]): Behavior[Command] = Behaviors.setup[Command] { ctx => def behavior(chatId: String, stateReporter: ActorRef[DeliveryStateChanged]): Behavior[Command] = checkParcel(chatId, stateReporter)
Behaviors.withTimers(scheduler => checkParcel(chatId, stateReporter, scheduler))
}
private def checkParcel(chatId: String, stateReporter: ActorRef[DeliveryStateChanged], scheduler: TimerScheduler[Command]): Behavior[Command] = Behaviors.setup { ctx => private def checkParcel(chatId: String, stateReporter: ActorRef[DeliveryStateChanged]): Behavior[Command] = Behaviors.withTimers { scheduler =>
implicit val actorSystem: ActorSystem = ctx.system.toUntyped Behaviors.setup { ctx =>
implicit val actorSystem: ActorSystem = ctx.system.toClassic
implicit val executionContext: ExecutionContextExecutor = ctx.system.dispatchers.lookup(DispatcherSelector.default()) implicit val executionContext: ExecutionContextExecutor = ctx.system.dispatchers.lookup(DispatcherSelector.default())
implicit val materializer: ActorMaterializer = ActorMaterializer()
val http = Http() val http = Http()
val badSslConfig = AkkaSSLConfig().mapSettings(s => s.withLoose(s.loose val badSslConfig = AkkaSSLConfig().mapSettings(s => s.withLoose(s.loose
.withAcceptAnyCertificate(true) .withAcceptAnyCertificate(true)
@@ -215,11 +213,11 @@ object CzechPostDeliveryCheck {
case Success(parcelHistories) => case Success(parcelHistories) =>
parcelHistories.foreach(parcelHistory => ctx.self ! ParcelHistoryRetrieved(parcelHistory)) parcelHistories.foreach(parcelHistory => ctx.self ! ParcelHistoryRetrieved(parcelHistory))
case Failure(exception) => case Failure(exception) =>
ctx.log.error(exception, "Error checking parcel history.") ctx.log.error("Error checking parcel history.", exception)
} }
.andThen { .andThen {
case Success(_) => ctx.log.info("action=check_parcel_state result=success chat_id={} check_uri={}", chatId, checkUri) case Success(_) => ctx.log.info("action=check_parcel_state result=success chat_id={} check_uri={}", chatId, checkUri)
case Failure(exception) => ctx.log.error(exception, "action=check_parcel_state result=failure chat_id={} check_uri={}", chatId, checkUri) case Failure(exception) => ctx.log.error(s"action=check_parcel_state result=failure chat_id=$chatId check_uri=$checkUri", exception)
} }
} }
} }
@@ -267,10 +265,11 @@ object CzechPostDeliveryCheck {
} }
EventSourcedBehavior[Command, Event, State]( EventSourcedBehavior[Command, Event, State](
persistenceId = PersistenceId(s"$chatId-czechpost"), persistenceId = PersistenceId(entityType, chatId),
emptyState = State(), emptyState = State(),
commandHandler = commandHandler, commandHandler = commandHandler,
eventHandler = eventHandler eventHandler = eventHandler
) )
} }
} }
}

View File

@@ -1,10 +1,7 @@
package eu.xeppaka.bot package eu.xeppaka.bot
import akka.actor.typed.scaladsl.Behaviors import akka.actor.typed.scaladsl.Behaviors
import akka.actor.typed.{ActorRef, Behavior, SupervisorStrategy} import akka.actor.typed.{ActorRef, Behavior}
import akka.persistence.typed.PersistenceId
import akka.persistence.typed.scaladsl.EventSourcedBehavior.{CommandHandler, EventHandler}
import akka.persistence.typed.scaladsl.{Effect, EffectBuilder, EventSourcedBehavior}
import akka.util.Timeout import akka.util.Timeout
import eu.xeppaka.bot.CheckDeliveryDialog.{ProcessMessageFailure, ProcessMessageSuccess} import eu.xeppaka.bot.CheckDeliveryDialog.{ProcessMessageFailure, ProcessMessageSuccess}
import eu.xeppaka.telegram.bot.TelegramEntities._ import eu.xeppaka.telegram.bot.TelegramEntities._
@@ -24,77 +21,34 @@ object DialogManager {
private case class DialogResponseSuccess(dialogId: Long, replyTo: ActorRef[CommandResult]) extends Command private case class DialogResponseSuccess(dialogId: Long, replyTo: ActorRef[CommandResult]) extends Command
private case class DialogResponseFailure(dialogId: Long, exception: Throwable, replyTo: ActorRef[CommandResult]) extends Command private case class DialogResponseFailure(dialogId: Long, exception: Throwable, replyTo: ActorRef[CommandResult]) extends Command
sealed trait Event
private case class DialogAdded(chatId: Long) extends Event
case class State(dialogs: Map[Long, ActorRef[CheckDeliveryDialog.Command]] = Map.empty)
def behavior(botUri: BotUri): Behavior[Command] = Behaviors.setup[Command] { ctx => def behavior(botUri: BotUri): Behavior[Command] = Behaviors.setup[Command] { ctx =>
val commandHandler: CommandHandler[Command, Event, State] = (state, cmd) => { Behaviors.receiveMessagePartial {
cmd match {
case ProcessUpdate(update, replyTo) => case ProcessUpdate(update, replyTo) =>
if (update.message.isDefined) { if (update.message.isDefined) {
val chatId = update.message.get.chat.id val chatId = update.message.get.chat.id
ctx.log.debug("action=process_update chat_id={} message={}", chatId, update.message.get)
val effect: EffectBuilder[Event, State] = if (state.dialogs.contains(chatId)) {
Effect.none
} else {
Effect.persist(DialogAdded(chatId))
}
effect
.thenRun(_ => ctx.log.debug("action=process_update chat_id={} message={}", chatId, update.message.get))
.thenRun { state =>
val msg = update.message.get val msg = update.message.get
val dialogActor = state.dialogs(chatId) 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) ctx.log.info("action=ask_dialog id={}", chatId)
implicit val timeout: Timeout = 20.seconds implicit val timeout: Timeout = 5.seconds
ctx.ask(dialogActor)((CheckDeliveryDialog.ProcessMessage.apply _).curried(msg)) { ctx.ask[CheckDeliveryDialog.Command, CheckDeliveryDialog.CommandResult](dialogActor, replyTo => CheckDeliveryDialog.ProcessMessage(msg, replyTo)) {
case Success(ProcessMessageSuccess) => DialogResponseSuccess(chatId, replyTo) case Success(ProcessMessageSuccess) => DialogResponseSuccess(chatId, replyTo)
case Success(ProcessMessageFailure(exception)) => DialogResponseFailure(chatId, exception, replyTo) case Success(ProcessMessageFailure(exception)) => DialogResponseFailure(chatId, exception, replyTo)
case Failure(exception) => DialogResponseFailure(chatId, exception, replyTo) case Failure(exception) => DialogResponseFailure(chatId, exception, replyTo)
} }
}
} else { } else {
Effect
.none
.thenRun { _ =>
ctx.log.debug("action=process_update result=success message=update message is empty") ctx.log.debug("action=process_update result=success message=update message is empty")
} }
} Behaviors.same
case DialogResponseSuccess(dialogId, replyTo) => case DialogResponseSuccess(dialogId, replyTo) =>
Effect
.none
.thenRun { _ =>
ctx.log.info("action=ask_dialog id={} result=success", dialogId) ctx.log.info("action=ask_dialog id={} result=success", dialogId)
replyTo ! ProcessUpdateSuccess replyTo ! ProcessUpdateSuccess
} Behaviors.same
case DialogResponseFailure(dialogId, exception, replyTo) => case DialogResponseFailure(dialogId, exception, replyTo) =>
Effect ctx.log.error(s"action=ask_dialog id=$dialogId result=failure", exception)
.none
.thenRun { _ =>
ctx.log.error(exception, "action=ask_dialog id={} result=failure", dialogId)
replyTo ! ProcessUpdateFailure(exception) replyTo ! ProcessUpdateFailure(exception)
Behaviors.same
} }
} }
} }
val eventHandler: EventHandler[State, Event] = (state, evt) => {
evt match {
case DialogAdded(chatId) =>
val dialogActor = ctx.spawn(Behaviors.supervise(CheckDeliveryDialog.behavior(chatId, botUri)).onFailure(SupervisorStrategy.restart), s"delivery-check-$chatId")
state.copy(dialogs = state.dialogs.updated(chatId, dialogActor))
}
}
EventSourcedBehavior(
persistenceId = PersistenceId("dialog-manager"),
emptyState = State(),
commandHandler = commandHandler,
eventHandler = eventHandler
)
}
}

View File

@@ -5,16 +5,15 @@ import java.security.{KeyStore, SecureRandom}
import java.util.UUID import java.util.UUID
import akka.Done import akka.Done
import akka.actor.ActorSystem
import akka.actor.typed.scaladsl.Behaviors
import akka.actor.typed.scaladsl.adapter._ import akka.actor.typed.scaladsl.adapter._
import akka.actor.typed.scaladsl.{Behaviors, StashBuffer} import akka.actor.typed._
import akka.actor.typed.{ActorRef, Behavior, DispatcherSelector, SupervisorStrategy}
import akka.actor.{ActorSystem, Scheduler}
import akka.http.scaladsl.marshalling.Marshal import akka.http.scaladsl.marshalling.Marshal
import akka.http.scaladsl.model._ import akka.http.scaladsl.model._
import akka.http.scaladsl.server.Directives.{as, complete, entity, extractLog, onComplete, path, post} import akka.http.scaladsl.server.Directives.{as, complete, entity, extractLog, onComplete, path, post}
import akka.http.scaladsl.server.Route import akka.http.scaladsl.server.Route
import akka.http.scaladsl.{ConnectionContext, Http, HttpExt, HttpsConnectionContext} import akka.http.scaladsl.{ConnectionContext, Http, HttpExt, HttpsConnectionContext}
import akka.stream.ActorMaterializer
import akka.util.{ByteString, Timeout} import akka.util.{ByteString, Timeout}
import eu.xeppaka.telegram.bot.TelegramEntities._ import eu.xeppaka.telegram.bot.TelegramEntities._
import javax.net.ssl.{KeyManagerFactory, SSLContext, TrustManagerFactory} import javax.net.ssl.{KeyManagerFactory, SSLContext, TrustManagerFactory}
@@ -34,11 +33,11 @@ object TelegramBot {
case object GetBotInfo case object GetBotInfo
case object GetWebhookInfo case object GetWebhookInfo
def behavior(botId: String, interface: String, localPort: Int, hookDomain: String, hookPort: Int, useHttpsServer: Boolean = true): Behavior[Command] = Behaviors.setup[Command] { ctx => 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") ctx.log.info("action=start_bot")
implicit val untypedSystem: ActorSystem = ctx.system.toClassic implicit val untypedSystem: ActorSystem = ctx.system.toClassic
implicit val actorMaterializer: ActorMaterializer = ActorMaterializer()
implicit val executionContextExecutor: ExecutionContextExecutor = ctx.system.dispatchers.lookup(DispatcherSelector.default()) implicit val executionContextExecutor: ExecutionContextExecutor = ctx.system.dispatchers.lookup(DispatcherSelector.default())
val botUri = BotUri(botId) val botUri = BotUri(botId)
@@ -46,9 +45,8 @@ object TelegramBot {
val hookId = UUID.randomUUID().toString val hookId = UUID.randomUUID().toString
val webhookUri = Uri(s"https://$hookDomain:$hookPort/$hookId") val webhookUri = Uri(s"https://$hookDomain:$hookPort/$hookId")
val httpsContext = if (useHttpsServer) Some(createHttpsConnectionContext) else None val httpsContext = if (useHttpsServer) Some(createHttpsConnectionContext) else None
val stashBuffer = StashBuffer[Command](10)
val dialogManager = ctx.spawnAnonymous(Behaviors.supervise(DialogManager.behavior(botUri)).onFailure(SupervisorStrategy.restart)) val dialogManager = ctx.spawnAnonymous(Behaviors.supervise(DialogManager.behavior(botUri)).onFailure(SupervisorStrategy.restart))
val routes = botRoutes(hookId, dialogManager)(untypedSystem.scheduler) val routes = botRoutes(hookId, dialogManager)(ctx.system.scheduler)
def bindingServer: Behavior[Command] = Behaviors.setup[Command] { ctx => def bindingServer: Behavior[Command] = Behaviors.setup[Command] { ctx =>
case class BindingSuccess(binding: Http.ServerBinding) extends Command case class BindingSuccess(binding: Http.ServerBinding) extends Command
@@ -142,10 +140,10 @@ object TelegramBot {
Behaviors.receiveMessage { Behaviors.receiveMessage {
case SetWebhookSuccess => case SetWebhookSuccess =>
ctx.log.info("action=set_webhook result=success") ctx.log.info("action=set_webhook result=success")
stashBuffer.unstashAll(ctx, started(binding)) stashBuffer.unstashAll(started(binding))
case SetWebhookFailure(exception) => case SetWebhookFailure(exception) =>
if (attempt > 20) { if (attempt > 20) {
ctx.log.error(exception, "action=set_webhook result=failure attempt={}", attempt) ctx.log.error(s"action=set_webhook result=failure attempt=$attempt", exception)
ctx.log.error("action=start_bot result=failure") ctx.log.error("action=start_bot result=failure")
unbindingServer(binding, None) unbindingServer(binding, None)
} else { } else {
@@ -192,7 +190,7 @@ object TelegramBot {
ctx.log.info("action=start_bot result=success") ctx.log.info("action=start_bot result=success")
Behaviors.receiveMessage[Command] { Behaviors.receiveMessage[Command] {
case stopCommand@Stop(replyTo) => case Stop(replyTo) =>
ctx.log.info("action=stop_bot") ctx.log.info("action=stop_bot")
deletingWebhook(binding, replyTo) deletingWebhook(binding, replyTo)
case _ => case _ =>
@@ -202,6 +200,7 @@ object TelegramBot {
bindingServer bindingServer
} }
}
private def botRoutes(hookId: String, updatesProcessor: ActorRef[DialogManager.ProcessUpdate])(implicit scheduler: Scheduler): Route = { private def botRoutes(hookId: String, updatesProcessor: ActorRef[DialogManager.ProcessUpdate])(implicit scheduler: Scheduler): Route = {
import akka.actor.typed.scaladsl.AskPattern._ import akka.actor.typed.scaladsl.AskPattern._
@@ -214,7 +213,7 @@ object TelegramBot {
post { post {
extractLog { log => extractLog { log =>
entity(as[Update]) { update => entity(as[Update]) { update =>
onComplete(updatesProcessor.?[DialogManager.CommandResult](ref => DialogManager.ProcessUpdate(update, ref))) { onComplete(updatesProcessor.ask[DialogManager.CommandResult](ref => DialogManager.ProcessUpdate(update, ref))) {
case Success(processResult) => processResult match { case Success(processResult) => processResult match {
case DialogManager.ProcessUpdateSuccess => complete(HttpResponse(status = StatusCodes.OK)) case DialogManager.ProcessUpdateSuccess => complete(HttpResponse(status = StatusCodes.OK))
case DialogManager.ProcessUpdateFailure(exception) => case DialogManager.ProcessUpdateFailure(exception) =>