From d3a7e77c1306bec86c8bb5212cf1a41a8ecdf142 Mon Sep 17 00:00:00 2001 From: Pavel Kachalouski Date: Sat, 18 May 2019 01:37:37 +0200 Subject: [PATCH] Working on making docker image --- build.sbt | 11 +- project/Dependencies.scala | 34 +- project/build.properties | 2 +- .../src/main/resources/application.conf | 18 - telegram-bot/src/main/resources/logback.xml | 11 - .../src/main/resources/telegram-bot.p12 | Bin 2485 -> 0 bytes .../src/main/resources/telegram-bot.pem | 21 -- .../main/scala/eu/xeppaka/bot/BotUri.scala | 15 - .../eu/xeppaka/bot/CheckDeliveryDialog.scala | 298 ----------------- .../xeppaka/bot/CzechPostDeliveryCheck.scala | 250 -------------- .../scala/eu/xeppaka/bot/DialogManager.scala | 100 ------ .../src/main/scala/eu/xeppaka/bot/Main.scala | 44 --- .../main/scala/eu/xeppaka/bot/PostType.scala | 8 - .../scala/eu/xeppaka/bot/TelegramBot.scala | 244 -------------- .../eu/xeppaka/bot/TelegramEntities.scala | 313 ------------------ .../bot/TelegramEntitiesDerivations.scala | 24 -- 16 files changed, 31 insertions(+), 1362 deletions(-) delete mode 100644 telegram-bot/src/main/resources/application.conf delete mode 100644 telegram-bot/src/main/resources/logback.xml delete mode 100644 telegram-bot/src/main/resources/telegram-bot.p12 delete mode 100644 telegram-bot/src/main/resources/telegram-bot.pem delete mode 100644 telegram-bot/src/main/scala/eu/xeppaka/bot/BotUri.scala delete mode 100644 telegram-bot/src/main/scala/eu/xeppaka/bot/CheckDeliveryDialog.scala delete mode 100644 telegram-bot/src/main/scala/eu/xeppaka/bot/CzechPostDeliveryCheck.scala delete mode 100644 telegram-bot/src/main/scala/eu/xeppaka/bot/DialogManager.scala delete mode 100644 telegram-bot/src/main/scala/eu/xeppaka/bot/Main.scala delete mode 100644 telegram-bot/src/main/scala/eu/xeppaka/bot/PostType.scala delete mode 100644 telegram-bot/src/main/scala/eu/xeppaka/bot/TelegramBot.scala delete mode 100644 telegram-bot/src/main/scala/eu/xeppaka/bot/TelegramEntities.scala delete mode 100644 telegram-bot/src/main/scala/eu/xeppaka/bot/TelegramEntitiesDerivations.scala diff --git a/build.sbt b/build.sbt index 8422276..f75e68b 100644 --- a/build.sbt +++ b/build.sbt @@ -3,14 +3,15 @@ import Dependencies._ lazy val commonSettings = Seq( organization := "com.example", scalaVersion := "2.12.8", - version := "0.1.0-SNAPSHOT", + version := "1.0.0", mainClass := Some("eu.xeppaka.bot.Main") ) inThisBuild(commonSettings) -lazy val `telegram-bot` = (project in file("telegram-bot")) +lazy val `telegram-bot-delivery` = (project in file(".")) .settings( + name := "telegram-bot-delivery", libraryDependencies ++= Seq( scalaTest % Test, akka, @@ -23,6 +24,10 @@ lazy val `telegram-bot` = (project in file("telegram-bot")) circleGeneric, circleParser, circeAkkaHttp - ) + ), + Docker / defaultLinuxInstallLocation := "/opt/telegram-bot-delivery", + Docker / dockerExposedPorts := Seq(88, 8443), + Docker / dockerRepository := Some("registry.xeppaka.eu:443") ) + .enablePlugins(JavaServerAppPackaging) .enablePlugins(DockerPlugin) diff --git a/project/Dependencies.scala b/project/Dependencies.scala index cc65768..c0cc943 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -1,16 +1,26 @@ import sbt._ +import Dependencies.Versions._ + object Dependencies { - lazy val akka = "com.typesafe.akka" %% "akka-actor" % "2.5.19" - lazy val akkaTyped = "com.typesafe.akka" %% "akka-actor-typed" % "2.5.19" - lazy val akkaStream = "com.typesafe.akka" %% "akka-stream" % "2.5.19" - lazy val akkaHttp = "com.typesafe.akka" %% "akka-http" % "10.1.5" - lazy val akkaPersistence = "com.typesafe.akka" %% "akka-persistence-typed" % "2.5.19" - lazy val levelDbJni = "org.fusesource.leveldbjni" % "leveldbjni-all" % "1.8" - //lazy val vkapi = "com.vk.api" % "sdk" % "0.5.12" - lazy val circleCore = "io.circe" %% "circe-core" % "0.10.0" - lazy val circleGeneric = "io.circe" %% "circe-generic" % "0.10.0" - lazy val circleParser = "io.circe" %% "circe-parser" % "0.10.0" - lazy val circeAkkaHttp = "de.heikoseeberger" %% "akka-http-circe" % "1.22.0" - lazy val scalaTest = "org.scalatest" %% "scalatest" % "3.0.5" + object Versions { + val akkaVersion = "2.5.22" + val akkaHttpVersion = "10.1.8" + val levelDbJniVersion = "1.8" + val circeVersion = "0.11.1" + val akkaHttpCirceVersion = "1.23.0" + val scalaTestVersion = "3.0.5" + } + + lazy val akka = "com.typesafe.akka" %% "akka-actor" % akkaVersion + lazy val akkaTyped = "com.typesafe.akka" %% "akka-actor-typed" % akkaVersion + lazy val akkaStream = "com.typesafe.akka" %% "akka-stream" % akkaVersion + lazy val akkaHttp = "com.typesafe.akka" %% "akka-http" % akkaHttpVersion + lazy val akkaPersistence = "com.typesafe.akka" %% "akka-persistence-typed" % akkaVersion + lazy val levelDbJni = "org.fusesource.leveldbjni" % "leveldbjni-all" % levelDbJniVersion + lazy val circleCore = "io.circe" %% "circe-core" % circeVersion + lazy val circleGeneric = "io.circe" %% "circe-generic" % circeVersion + lazy val circleParser = "io.circe" %% "circe-parser" % circeVersion + lazy val circeAkkaHttp = "de.heikoseeberger" %% "akka-http-circe" % akkaHttpCirceVersion + lazy val scalaTest = "org.scalatest" %% "scalatest" % scalaTestVersion } diff --git a/project/build.properties b/project/build.properties index 72f9028..c0bab04 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=1.2.7 +sbt.version=1.2.8 diff --git a/telegram-bot/src/main/resources/application.conf b/telegram-bot/src/main/resources/application.conf deleted file mode 100644 index 426474b..0000000 --- a/telegram-bot/src/main/resources/application.conf +++ /dev/null @@ -1,18 +0,0 @@ -akka { - loglevel = "DEBUG" - - extensions = [akka.persistence.Persistence] - - persistence { - journal { - plugin = "akka.persistence.journal.leveldb" - auto-start-journals = ["akka.persistence.journal.leveldb"] - leveldb.dir = "journal-check-delivery" - } - - snapshot-store { - plugin = "akka.persistence.snapshot-store.local" - auto-start-snapshot-stores = ["akka.persistence.snapshot-store.local"] - } - } -} diff --git a/telegram-bot/src/main/resources/logback.xml b/telegram-bot/src/main/resources/logback.xml deleted file mode 100644 index ab73bdc..0000000 --- a/telegram-bot/src/main/resources/logback.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - %date [%level] %logger: %message%n%xException - - - - - - - diff --git a/telegram-bot/src/main/resources/telegram-bot.p12 b/telegram-bot/src/main/resources/telegram-bot.p12 deleted file mode 100644 index 8147227e359838eaba30494f9a7d615f985eb377..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2485 zcmY+^c{~%2AIEXWXk(68u0PI# z27m*he`OaW4#5A<#7e{gfWH!eiID+`V?Oz3VMLy1W&8IBFe59H69Bx?{E6z_ZJ*1; z1Z0T8F_ZmmVf6IualJj4)-1Ge_Y8VP2YF#~S`ZR9IV#h&Ql*OG{%|Q6RTdNfju3QJ z$@0Wv3ztKBy}9p#Aed!$&b>{`kH1gPI}ST`iSMarGMF~ z<&QCwSMBnAJv|vrCe38@g-!r~4-moU(<}qS7wcY3$ewnRMvhgC3D43UT zO;TuQQo7C{C%61Bm24?ug?ziw|3JL2J6nR)rl+|e`BN!R9EDsM7PzERkmRE1ob ziWtAeDG1R^{&CM^gaXU#zuu6>p4x>babw+ru7vu^tN@X9ZeDre2({ppmc7(yRZ{dvp2ZNOk1E6uY)p9QyO9lD22M9v%a$OYNic4r!zX)9KCDta}=W=gRM z$IILj4!HZHdf?C0#cc%$1L#vIS0zH;^1G^2K6%!iD?0?=+;NdZLx2`(8{_^|ykaJ6 z-iKW;LQWM}pq?KQK>_4{)cV|5sbaM(zx_34e(5=J3=O4v?HO1|dT@X6YFQx3&(8b( zWmIzdth-I^#H(h5&`WxerV*WJ1C|6t^@2&fKA)hO@YrsnbEa;U)=$5z-tgeFAzU~r zZ;axnKp&Eu{^q5s=2f&0!ypR~ms=1>l5(8wLQm(Q$LOCabsO%ScpoD3R|3hhJ}r^j z+%g-BZwt}aSAi^>PwvUjnUi{bEl8kd~~r*=n4YNOi1h9>4+7X#S2h>`+6mb>gy z_h%W(E_b(PCn={#JO;dBL6-T+Yr)4$XqLtFT^o8Ur%3G&dJpFrMOAx>kXPWr$4utx zIO>AK(zg{)mtL`+SKXm8B)IL!QxGR=n8*sG4g~YLF{|^8}9E&0V z$0GMD!+#x&mHj^)urV_Ia%20K8yo+p1oLktwj#p#H^ay!zm)*s0KK~m=&%hF-it~q z@7ZTX9X77{M-Mnh7wLqMtWt)0q|Dk?HGG6pjOZwbCf?Pe`(fpQN}3# zL8^9zUb$wITZg!YPhCki+vC7hz*)HRY4K{@YiaEFs!u77`3;t>^8$Z-(`8WT;Da7k zTztG;^`_}FPk%##dPICEgGS$Ve=R?$MK&Wv$X_eYmD$=dVt!+mZ?}cOSx$I5Ur5wm zy1a@0)~2;kW$;c&;zaOVc*||0DP!hVDR)?)>CA~MUXA9uDtTwPfH!JK5&tyG!iaj! z&7f$`HG6PzQvbbkfZdudYx6_x$>pjNVwzj&&9OMm>vk2Rx5h4_k$Q7aJ06J7D#Hp6 z4Xz(sU?kRNPcL%>q0+~@&dO)QQ}v~)0RzcfcwGfu0YTY?^MXnBA?YEf)8@5Le6nZ@ z99tEC`>qrsCGwq>G&*N4xV<}OL)JYpQ^O-DxKmmIk7aIXBNrUt5RvL&&Y3V%^`Q-*Rr3 zt!-J^h}PP?YRLYn;1%}~)e~608mZYcBR4MpD5l9_ zqQSBSuD63EYbJcGh=-dhma4JMmRXfr!#dbx5BDtr>jPR8$2x3U$WzHbypbbw0Ch#; zZ`&aJou$OQYpID8W22PPN2An?oVH}^Gu{N4guw<$MjU%0WE9BDgX(%hK>g$9aAD;A^OhHfZ^{T4a@`$-GBc6j-{0k90-;=I*Z1G^6G;V*9 zrC1eYg4G}Q-4|@MesA7?(Zm(1VnifP0A9RFZ&w(ds!UMq{<-Q`dbJ0q<`DBx18M{jq`Oc z0Zs`v%yVM)E7oU)|qJVLx!`&+CYcueDE&Zb=Z<+j6g9$9>zbEO!Fj=`b29;o}S z=PI>~egS3fML2$Z^wo1FbwQO#BY{`tg?uIUF{DlVq&M9CXkD6J( zr{9#P@fdR(t@HaSU)v_J9O~3g*54@~h>_)G+i_1ph{PHgbC#zKy_opqQgg{7Q^0Yy z$~zD$XWdG`+)wR?j>aHE#C zn7*I_2VOqq>B5@@NOnUJH{21UF`3~uTdRIy!&IO{&!uu*jD>Q7lemK@dt`AuJmP#v zqa)VR5+KG4P-sa#4B!_>r~2ZRFpFR=t;h#L5_rui_+9N7<1FFzL0V-RaWL}5`iW&N<5QbmfBXs;v%38eUcbp)ML-~&KpYdvrwOzJvXKxy>5gN?LDY{i{O1*9kv w#L6PW$;>3g!T^A{E>RU~1E9&n&U@!*9-DW)5Y#RJkjXSY4^-hA^;^k*0YP7v3;+NC diff --git a/telegram-bot/src/main/resources/telegram-bot.pem b/telegram-bot/src/main/resources/telegram-bot.pem deleted file mode 100644 index 0fe289b..0000000 --- a/telegram-bot/src/main/resources/telegram-bot.pem +++ /dev/null @@ -1,21 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDcTCCAlmgAwIBAgIJAKLMfxW4KRHuMA0GCSqGSIb3DQEBCwUAME8xCzAJBgNV -BAYTAkNaMQ8wDQYDVQQHDAZQcmFndWUxGjAYBgNVBAoMEVBhdmVsIEthY2hhbG91 -c2tpMRMwEQYDVQQDDAp4ZXBwYWthLmV1MB4XDTE4MDUxMTE4MjEzOVoXDTI4MDUw -ODE4MjEzOVowTzELMAkGA1UEBhMCQ1oxDzANBgNVBAcMBlByYWd1ZTEaMBgGA1UE -CgwRUGF2ZWwgS2FjaGFsb3Vza2kxEzARBgNVBAMMCnhlcHBha2EuZXUwggEiMA0G -CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDWwewslKtWJ7yeYrxDuoh5PS7y5/C/ -0NT6tsAAsh3ZVCqdeNvYj56n8jVob/jJ9EYMqKj7dXLAMopDhtuwdDN/KhW9QVkK -zATE1wNxuV3aBVUTJuHHadUYQa7pVevvssAIa1XQ6NvU0pkwdDApylOj1TkA9MFl -ZWHWlF0dgrVyGjFxDoWdjm2aLCdRpZCr0giTOfZ5E+OJNALTHcuJO+PRKdEreO1Y -VAlT2Sk26f8/iG63C2/t7xWTyJKOjFPxwq3+dkNfJ1AXZ4I7aFDgP7BKogvooYuC -BItqog+IRUOoK9Yj24KCUxD+gaI5+tv0j1ov5d0ZAqqaiSql96s2/jyZAgMBAAGj -UDBOMB0GA1UdDgQWBBRkOXFj0c0jNdM1nJMRGr0EvfeMuTAfBgNVHSMEGDAWgBRk -OXFj0c0jNdM1nJMRGr0EvfeMuTAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBCwUA -A4IBAQBtXUOfnKdp1GY5gATTrPdr2s7FyiJvVfx/yeNNNR8ZnZcLjBMulEvXsfNi -AL1cEctnSDyT5z2el75nTdAgTFcBZQLsVk9/4ZwBRnfunFqfU5e5X9X9z//yt4Gy -Zq9BrMddQE+qwnOclcTDCc0GnyqKbaPiyYFcaXKhdrsflvoJI9tyLwPgjfXADLDF -JtjC0gGdbgefDweBUMTF0cpZED9q/J2fKXHurub+3QySvUOvphVFP4dBz2WhdoTe -v3lkEVp3I/IUv9qegO0B0o6X+Nnml4/b7HV1PArNceWOA6f57fSL2m6eN6xs4ULJ -kfUMloAr25yvmN/tPwm+8Op5ovot ------END CERTIFICATE----- diff --git a/telegram-bot/src/main/scala/eu/xeppaka/bot/BotUri.scala b/telegram-bot/src/main/scala/eu/xeppaka/bot/BotUri.scala deleted file mode 100644 index 100fddd..0000000 --- a/telegram-bot/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/telegram-bot/src/main/scala/eu/xeppaka/bot/CheckDeliveryDialog.scala b/telegram-bot/src/main/scala/eu/xeppaka/bot/CheckDeliveryDialog.scala deleted file mode 100644 index e1a5713..0000000 --- a/telegram-bot/src/main/scala/eu/xeppaka/bot/CheckDeliveryDialog.scala +++ /dev/null @@ -1,298 +0,0 @@ -package eu.xeppaka.bot - -import akka.actor.typed.scaladsl.adapter._ -import akka.actor.typed.scaladsl.{Behaviors, StashBuffer} -import akka.actor.typed.{ActorRef, Behavior, DispatcherSelector, SupervisorStrategy} -import akka.http.scaladsl.Http -import akka.http.scaladsl.model._ -import akka.stream.ActorMaterializer -import akka.stream.scaladsl.{Sink, Source} -import akka.util.{ByteString, Timeout} -import eu.xeppaka.bot.TelegramEntities._ -import eu.xeppaka.bot.TelegramEntitiesDerivations._ -import io.circe.Printer - -import scala.concurrent.ExecutionContext -import scala.concurrent.duration._ -import scala.util.{Failure, Success} - -object CheckDeliveryDialog { - 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 "/start" => Help - case _ => Help - } - } - - // json printer - private val printer = Printer.noSpaces.copy(dropNullValues = true) - // 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) - )) - - def behavior(chatId: Long, botUri: BotUri): Behavior[Command] = Behaviors.setup[Command] { ctx => - implicit val materializer: ActorMaterializer = ActorMaterializer()(ctx.system.toUntyped) - implicit val executionContext: ExecutionContext = ctx.system.dispatchers.lookup(DispatcherSelector.default()) - val http = Http()(ctx.system.toUntyped) - val stashBuffer = StashBuffer[Command](100) - 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 initial: Behavior[Command] = sendMessage(SendMessage(chatId, "Waiting for a command...", reply_markup = commandsKeyboard), waitCommand, initial) - - 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, initial, initial) - } - case AddParcel => - val message = SendMessage(chatId, "Please enter a parcel ID.") - sendMessage(message, waitParcelId(parcelId => addParcel(parcelId)), initial) - case RemoveParcel => - removeParcel(initial, initial) - case ListParcels => - listParcels - case Help => - val message = SendMessage(chatId, helpMessage) - sendMessage(message, initial, initial) - case DeliveryStateChanged(state) => - val message = SendMessage(chatId, state, Some("Markdown")) - sendMessage(message, initial, initial) - case _ => - Behaviors.unhandled - } - - def addParcel(parcelId: String): Behavior[Command] = 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, 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.") - sendMessage(message, initial, initial) - case AddParcelFailure(exception) => - exception match { - case CzechPostDeliveryCheck.DuplicateParcelId(_) => - val message = SendMessage(chatId, s"Parcel $parcelId is in the watch list already.") - sendMessage(message, initial, initial) - case _ => - ctx.log.error(exception, "action=add_parcel result=failure") - val message = SendMessage(chatId, s"Adding parcel failed. Please try again.") - sendMessage(message, initial, initial) - } - case otherMessage => - stashBuffer.stash(otherMessage) - Behaviors.same - } - } - - def listParcels: Behavior[Command] = Behaviors.setup { ctx => - case class ListParcelsSuccess(parcelsList: Set[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.toSeq.sorted.mkString("\n") else "(empty)") - val message = SendMessage(chatId, messageText, Some("Markdown")) - sendMessage(message, initial, initial) - case ListParcelsFailure(exception) => - ctx.log.error(exception, "action=list_parcels result=failure chat_id={}", chatId) - val message = SendMessage(chatId, "Failed to get a list of your watched parcels. Please try again later.") - sendMessage(message, initial, initial) - case otherMessage => - stashBuffer.stash(otherMessage) - Behaviors.same - } - } - - def removeParcel(onSuccess: => Behavior[Command], onFailure: => Behavior[Command]): Behavior[Command] = - Behaviors.setup { ctx => - case class ListParcelsSuccess(parcelsList: Set[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) => - if (parcelsList.nonEmpty) { - val keyboardButtons = parcelsList.toSeq.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, waitParcelId(parcelId => removeParcelId(parcelId)), onFailure) - } else { - val message = SendMessage(chatId, "You don't have watched parcels. There is nothing to remove.") - sendMessage(message, onSuccess, onFailure) - } - case ListParcelsFailure(exception) => - ctx.log.error(exception, "action=list_parcels result=failure chat_id={}", chatId) - val message = SendMessage(chatId, "Failed to get a list of your watched parcels. Please try again later.") - sendMessage(message, initial, initial) - case otherMessage => - stashBuffer.stash(otherMessage) - Behaviors.same - } - } - - def removeParcelId(parcelId: String): Behavior[Command] = 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.") - sendMessage(message, initial, initial) - 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.") - sendMessage(message, initial, initial) - case _ => - ctx.log.error(exception, "action=add_parcel result=failure") - val message = SendMessage(chatId, s"Remove of the parcel failed. Please try again.") - sendMessage(message, initial, initial) - } - 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)), initial) - // } - - def waitParcelId(onFinish: String => Behavior[Command]): Behavior[Command] = Behaviors.receiveMessage { - case ProcessMessage(msg, replyTo) => - if (msg.text.isDefined) { - val parcelId = msg.text.get - replyTo ! ProcessMessageSuccess - onFinish(parcelId) - } else { - replyTo ! ProcessMessageSuccess - waitParcelId(onFinish) - } - case otherMsg => - stashBuffer.stash(otherMsg) - Behaviors.same - } - - def sendMessage(message: SendMessage, onSuccess: => Behavior[Command], onFailure: => Behavior[Command], attempt: Int = 0): Behavior[Command] = Behaviors.setup[Command] { ctx => - import io.circe.generic.auto._ - import io.circe.syntax._ - - case object SendMessageSuccess extends Command - case class SendMessageFailure(exception: Throwable) extends Command - - val json = printer.pretty(message.asJson) - val request = HttpRequest(HttpMethods.POST, uri = botUri.sendMessage, entity = HttpEntity.Strict(ContentTypes.`application/json`, ByteString(json))) - - ctx.log.debug("action=send_message status=started chat_id={} message={}", chatId, json) - - Source - .single(request) - .initialDelay(2.seconds * attempt) - .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(exception, "action=send_message status=finished result=failure chat_id={}", chatId) - 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(ctx, onSuccess) - case SendMessageFailure(exception) => - ctx.log.error(exception, "action=send_message status=finished result=failure chat_id={} attempt={}", chatId, attempt) - - if (attempt >= 5) { - ctx.log.error(exception, "action=send_message result=failure message=attempts threshold exceeded") - stashBuffer.unstashAll(ctx, onFailure) - } else { - sendMessage(message, onSuccess, onFailure, attempt + 1) - } - case otherMsg => - stashBuffer.stash(otherMsg) - Behaviors.same - } - } - - initial - } -} diff --git a/telegram-bot/src/main/scala/eu/xeppaka/bot/CzechPostDeliveryCheck.scala b/telegram-bot/src/main/scala/eu/xeppaka/bot/CzechPostDeliveryCheck.scala deleted file mode 100644 index 74b156c..0000000 --- a/telegram-bot/src/main/scala/eu/xeppaka/bot/CzechPostDeliveryCheck.scala +++ /dev/null @@ -1,250 +0,0 @@ -package eu.xeppaka.bot - -import java.security.cert.X509Certificate -import java.text.SimpleDateFormat - -import akka.actor.ActorSystem -import akka.actor.typed.scaladsl.adapter._ -import akka.actor.typed.scaladsl.{Behaviors, TimerScheduler} -import akka.actor.typed.{ActorRef, Behavior, DispatcherSelector} -import akka.http.scaladsl.UseHttp2.Negotiated -import akka.http.scaladsl.model._ -import akka.http.scaladsl.model.headers.{Accept, `User-Agent`} -import akka.http.scaladsl.settings.{ClientConnectionSettings, ConnectionPoolSettings} -import akka.http.scaladsl.unmarshalling.Unmarshal -import akka.http.scaladsl.{Http, HttpsConnectionContext} -import akka.persistence.typed.PersistenceId -import akka.persistence.typed.scaladsl.EventSourcedBehavior.{CommandHandler, EventHandler} -import akka.persistence.typed.scaladsl.{Effect, EventSourcedBehavior} -import akka.stream.ActorMaterializer -import com.typesafe.sslconfig.akka.AkkaSSLConfig -import de.heikoseeberger.akkahttpcirce.FailFastCirceSupport._ -import io.circe.generic.auto._ -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 { - private val czechPostDateFormat = new SimpleDateFormat("yyyy-MM-dd") - private val printDateFormat = new SimpleDateFormat("dd-MM-yyyy") - - sealed trait Command - sealed trait CommandResult - sealed trait Event - case class ParcelState(attributes: Option[Entities.Attributes] = None, states: Set[Entities.State] = Set.empty) { - def prettyPrint(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:* - |=========================== - |$statesString""".stripMargin - } - } - case class State(parcelStates: Map[String, ParcelState] = Map.empty) - - case class AddParcel(parcelId: 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: Set[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) 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] = Behaviors.setup[Command] { ctx => - Behaviors.withTimers(scheduler => checkParcel(chatId, stateReporter, scheduler)) - } - - private def checkParcel(chatId: String, stateReporter: ActorRef[DeliveryStateChanged], scheduler: TimerScheduler[Command]): Behavior[Command] = Behaviors.setup { ctx => - implicit val actorSystem: ActorSystem = ctx.system.toUntyped - implicit val executionContext: ExecutionContextExecutor = ctx.system.dispatchers.lookup(DispatcherSelector.default()) - implicit val materializer: ActorMaterializer = ActorMaterializer() - val http = Http() - val badSslConfig = AkkaSSLConfig().mapSettings(s => s.withLoose(s.loose - .withAcceptAnyCertificate(true) - .withDisableHostnameVerification(true))) - val originalCtx = http.createClientHttpsContext(badSslConfig) - val sslContext = new HttpsConnectionContext( - trustfulSslContext, - originalCtx.sslConfig, - originalCtx.enabledCipherSuites, - originalCtx.enabledProtocols, - originalCtx.clientAuth, - originalCtx.sslParameters, - Negotiated - ) - 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.startPeriodicTimer("check-delivery-state", CheckParcels, 5.minutes) - - val commandHandler: CommandHandler[Command, Event, State] = (state, cmd) => { - cmd match { - case AddParcel(parcelId, replyTo) => - val parcelIdUpper = parcelId.toUpperCase - if (state.parcelStates.keySet.contains(parcelIdUpper)) { - Effect - .none - .thenRun(_ => replyTo ! CommandResultFailure(DuplicateParcelId(parcelIdUpper))) - } else { - Effect - .persist(ParcelAdded(parcelIdUpper)) - .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.parcelStates.keySet - replyTo ! ListParcelsResult(parcelsList) - } - - case CheckParcels => - Effect - .none - .thenRun { _ => - ctx.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`))) - - ctx.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[Array[Entities.ParcelHistory]]) - .andThen { - case Success(parcelHistories) => - parcelHistories.foreach(parcelHistory => ctx.self ! ParcelHistoryRetrieved(parcelHistory)) - case Failure(exception) => - ctx.log.error(exception, "Error checking parcel history.") - } - .andThen { - 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 ParcelHistoryRetrieved(parcelHistory) => - val parcelId = parcelHistory.id - val parcelState = state.parcelStates(parcelId) - val attributesChangedEvent = (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)).to[collection.immutable.Seq] - - val newStates = parcelHistory.states.state.toSet -- parcelState.states - val stateEvents: Seq[Event] = newStates.map(state => ParcelHistoryStateAdded(parcelId, state)).to[collection.immutable.Seq] - - Effect - .persist(attributesChangedEvent ++ stateEvents) - .thenRun(_ => { - if (newStates.nonEmpty) { - stateReporter ! DeliveryStateChanged(ParcelState(None, newStates).prettyPrint(parcelId)) - } - }) - } - } - - val eventHandler: EventHandler[State, Event] = (state, evt) => { - evt match { - case ParcelAdded(parcelId) => state.copy(parcelStates = state.parcelStates + (parcelId -> ParcelState())) - 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(s"$chatId-czechpost"), - emptyState = State(), - commandHandler = commandHandler, - eventHandler = eventHandler - ) - } -} diff --git a/telegram-bot/src/main/scala/eu/xeppaka/bot/DialogManager.scala b/telegram-bot/src/main/scala/eu/xeppaka/bot/DialogManager.scala deleted file mode 100644 index d21eeaa..0000000 --- a/telegram-bot/src/main/scala/eu/xeppaka/bot/DialogManager.scala +++ /dev/null @@ -1,100 +0,0 @@ -package eu.xeppaka.bot - -import akka.actor.typed.scaladsl.Behaviors -import akka.actor.typed.{ActorRef, Behavior, SupervisorStrategy} -import akka.persistence.typed.PersistenceId -import akka.persistence.typed.scaladsl.EventSourcedBehavior.{CommandHandler, EventHandler} -import akka.persistence.typed.scaladsl.{Effect, EventSourcedBehavior} -import akka.util.Timeout -import eu.xeppaka.bot.CheckDeliveryDialog.{ProcessMessageFailure, ProcessMessageSuccess} -import eu.xeppaka.bot.TelegramEntities.Update - -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 - - 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 => - val commandHandler: CommandHandler[Command, Event, State] = (state, cmd) => { - cmd match { - case ProcessUpdate(update, replyTo) => - if (update.message.isDefined) { - val chatId = update.message.get.chat.id - - val effect: Effect[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 dialogActor = state.dialogs(chatId) - - ctx.log.info("action=ask_dialog id={}", chatId) - - implicit val timeout: Timeout = 20.seconds - ctx.ask(dialogActor)((CheckDeliveryDialog.ProcessMessage.apply _).curried(msg)) { - case Success(ProcessMessageSuccess) => DialogResponseSuccess(chatId, replyTo) - case Success(ProcessMessageFailure(exception)) => DialogResponseFailure(chatId, exception, replyTo) - case Failure(exception) => DialogResponseFailure(chatId, exception, replyTo) - } - } - } else { - Effect - .none - .thenRun { _ => - ctx.log.debug("action=process_update result=success message=update message is empty") - } - } - - case DialogResponseSuccess(dialogId, replyTo) => - Effect - .none - .thenRun { _ => - ctx.log.info("action=ask_dialog id={} result=success", dialogId) - replyTo ! ProcessUpdateSuccess - } - case DialogResponseFailure(dialogId, exception, replyTo) => - Effect - .none - .thenRun { _ => - ctx.log.error(exception, "action=ask_dialog id={} result=failure", dialogId) - replyTo ! ProcessUpdateFailure(exception) - } - } - } - - 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 - ) - } -} diff --git a/telegram-bot/src/main/scala/eu/xeppaka/bot/Main.scala b/telegram-bot/src/main/scala/eu/xeppaka/bot/Main.scala deleted file mode 100644 index 142f535..0000000 --- a/telegram-bot/src/main/scala/eu/xeppaka/bot/Main.scala +++ /dev/null @@ -1,44 +0,0 @@ -package eu.xeppaka.bot - -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.{Done, actor} - -import scala.concurrent.duration._ -import scala.concurrent.{Await, ExecutionContextExecutor, Future} -import scala.io.StdIn - -object Main { - def main(args: Array[String]): Unit = { - val isProduction = System.getProperty("isProduction", "false").toBoolean - - val (botId, localPort, hookPort) = if (isProduction) { - ("693134480:AAE8JRXA6j1mkOKTaxapP6A-E4LPHRuiIf8", 88, 88) // delivery bot - } else { - ("570855144:AAEv7b817cuq2JJI9f2kG5B9G3zW1x-btz4", 8443, 8443) // useless bot - } - - val botBehavior = Behaviors.supervise(TelegramBot.behavior(botId, "0.0.0.0", localPort, hookPort)).onFailure(SupervisorStrategy.restart) - val telegramBot = ActorSystem(botBehavior, "telegram-bot") - 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/telegram-bot/src/main/scala/eu/xeppaka/bot/PostType.scala b/telegram-bot/src/main/scala/eu/xeppaka/bot/PostType.scala deleted file mode 100644 index ae47a25..0000000 --- a/telegram-bot/src/main/scala/eu/xeppaka/bot/PostType.scala +++ /dev/null @@ -1,8 +0,0 @@ -package eu.xeppaka.bot - -sealed trait PostType - -object PostTypes { - case object CzechPost extends PostType - case object PplPost extends PostType -} diff --git a/telegram-bot/src/main/scala/eu/xeppaka/bot/TelegramBot.scala b/telegram-bot/src/main/scala/eu/xeppaka/bot/TelegramBot.scala deleted file mode 100644 index 94a76b3..0000000 --- a/telegram-bot/src/main/scala/eu/xeppaka/bot/TelegramBot.scala +++ /dev/null @@ -1,244 +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, Scheduler} -import akka.actor.typed.scaladsl.adapter._ -import akka.actor.typed.scaladsl.{Behaviors, StashBuffer} -import akka.actor.typed.{ActorRef, Behavior, DispatcherSelector, SupervisorStrategy} -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.stream.ActorMaterializer -import akka.util.{ByteString, Timeout} -import eu.xeppaka.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, hookPort: Int): Behavior[Command] = Behaviors.setup[Command] { ctx => - ctx.log.info("action=start_bot") - - implicit val untypedSystem: ActorSystem = ctx.system.toUntyped - implicit val actorMaterializer: ActorMaterializer = ActorMaterializer() - 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://xeppaka.eu:$hookPort/$hookId") - val httpsContext = createHttpsConnectionContext - val stashBuffer = StashBuffer[Command](10) - val dialogManager = ctx.spawnAnonymous(Behaviors.supervise(DialogManager.behavior(botUri)).onFailure(SupervisorStrategy.restart)) - val routes = botRoutes(hookId, dialogManager)(untypedSystem.scheduler) - - def bindingServer: 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) - - http - .bindAndHandle(routes, interface, localPort, httpsContext) - .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") - settingWebhook(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 unbindingServer(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 settingWebhook(binding: Http.ServerBinding): 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 = Multipart.FormData.BodyPart.Strict("url", urlEntity) - - val certificate = ByteString(Source.fromResource("telegram-bot.pem").mkString) - val certificateEntity = HttpEntity.Strict(ContentTypes.`application/octet-stream`, certificate) - val certificatePart = Multipart.FormData.BodyPart.Strict("certificate", certificateEntity, Map("filename" -> "telegram-bot.pem")) - - val setWebhookFormData = Multipart.FormData.Strict(immutable.Seq(urlPart, certificatePart)) - - Marshal(setWebhookFormData) - .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(ctx, started(binding)) - case SetWebhookFailure(exception) => - ctx.log.error("action=set_webhook result=failure", exception) - ctx.log.error("action=start_bot result=failure") - unbindingServer(binding, None) - 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") - unbindingServer(binding, Some(replyTo)) - case DeleteWebhookFailure(exception) => - ctx.log.error("action=delete_webhook result=failure", exception) - unbindingServer(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 stopCommand@Stop(replyTo) => - ctx.log.info("action=stop_bot") - deletingWebhook(binding, replyTo) - case _ => - Behaviors.unhandled - } - } - - bindingServer - } - - private def botRoutes(hookId: String, updatesProcessor: ActorRef[DialogManager.ProcessUpdate])(implicit scheduler: Scheduler): Route = { - import akka.actor.typed.scaladsl.AskPattern._ - import de.heikoseeberger.akkahttpcirce.FailFastCirceSupport._ - import io.circe.generic.auto._ - - implicit val timeout: Timeout = 30.seconds - - path(hookId) { - post { - extractLog { log => - entity(as[Update]) { update => - onComplete(updatesProcessor.?[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/telegram-bot/src/main/scala/eu/xeppaka/bot/TelegramEntities.scala b/telegram-bot/src/main/scala/eu/xeppaka/bot/TelegramEntities.scala deleted file mode 100644 index b2075e0..0000000 --- a/telegram-bot/src/main/scala/eu/xeppaka/bot/TelegramEntities.scala +++ /dev/null @@ -1,313 +0,0 @@ -package eu.xeppaka.bot - -object TelegramEntities { - - case class Response[T](ok: Boolean, - description: Option[String] = None, - error_code: Option[Int] = None, - result: T - ) - - case class GetMe(id: Int, is_bot: Boolean, first_name: String, username: String) - - case class KeyboardButton(text: String, - request_contact: Option[Boolean] = None, - request_location: Option[Boolean] = None - ) - - case class InlineKeyboardButton(text: String, - url: Option[String] = None, - callback_data: Option[String] = None, - switch_inline_query: Option[String] = None, - switch_inline_query_current_chat: Option[String] = None, - callback_game: Option[String] = None, - pay: Option[Boolean] = None - ) - - sealed trait ReplyMarkup - - case class ReplyKeyboardRemove(remove_keyboard: Boolean = true, selective: Option[Boolean] = None) extends ReplyMarkup - - case class ReplyKeyboardMarkup(keyboard: Seq[Seq[KeyboardButton]], - resize_keyboard: Option[Boolean] = None, - one_time_keyboard: Option[Boolean] = None, - selective: Option[Boolean] = None - ) extends ReplyMarkup - - case class InlineKeyboardMarkup(inline_keyboard: Seq[Seq[InlineKeyboardButton]]) - extends ReplyMarkup - - case class ForceReply(force_reply: Boolean = true, selective: Option[Boolean] = None) extends ReplyMarkup - - case class InlineQuery(id: String, - from: User, - location: Location, - query: String, - offset: String - ) - - case class Location(longitude: Float, - latitude: Float - ) - - case class Update(update_id: Int, - message: Option[Message] = None, - edited_message: Option[Message] = None, - channel_post: Option[Message] = None, - edited_channel_post: Option[Message] = None, - inline_query: Option[InlineQuery] = None, - chosen_inline_result: Option[ChosenInlineResult] = None, - callback_query: Option[CallbackQuery] = None, - shipping_query: Option[ShippingQuery] = None, - pre_checkout_query: Option[PreCheckoutQuery] = None - ) - - case class ChosenInlineResult(result_id: String, - from: User, - location: Option[Location] = None, - inline_message_id: Option[String] = None, - query: String - ) - - case class CallbackQuery(id: String, - from: User, - message: Option[Message] = None, - inline_message_id: Option[String] = None, - chat_instance: String, - data: Option[String] = None, - game_short_name: Option[String] = None - ) - - case class ShippingQuery(id: String, - from: User, - invoice_payload: String, - shipping_address: ShippingAddress - ) - - case class ShippingAddress(country_code: String, - state: String, - city: String, - street_line1: String, - street_line2: String, - post_code: String - ) - - case class PreCheckoutQuery(id: String, - from: User, - currency: String, - total_amount: Int, - invoice_payload: String, - shipping_option_id: Option[String] = None, - order_info: Option[OrderInfo] = None - ) - - case class OrderInfo(name: Option[String] = None, - phone_number: Option[String] = None, - email: Option[String] = None, - shipping_address: Option[ShippingAddress] = None - ) - - case class User(id: Int, - is_bot: Boolean, - first_name: String, - last_name: Option[String] = None, - username: Option[String] = None, - language_code: Option[String] = None - ) - - case class EditMessageReplyMarkup(chat_id: Option[Long], - message_id: Option[Int], - inline_message_id: Option[String], - reply_markup: Option[InlineKeyboardMarkup] - ) - - case class SendMessage(chat_id: Long, - text: String, - parse_mode: Option[String] = None, - disable_web_page_preview: Option[Boolean] = None, - disable_notification: Option[Boolean] = None, - reply_to_message_id: Option[Int] = None, - reply_markup: Option[ReplyMarkup] = None - ) - - case class Message(message_id: Int, - from: Option[User] = None, - date: Int, - chat: Chat, - forward_from: Option[User] = None, - forward_from_chat: Option[User] = None, - forward_from_message_id: Option[Int] = None, - forward_signature: Option[String] = None, - forward_date: Option[Int] = None, - reply_to_message: Option[Message] = None, - edit_date: Option[Int] = None, - media_group_id: Option[String] = None, - author_signature: Option[String] = None, - text: Option[String] = None, - entities: Option[Seq[MessageEntity]] = None, - caption_entities: Option[Seq[MessageEntity]] = None, - audio: Option[Audio] = None, - document: Option[Document] = None, - game: Option[Game] = None, - photo: Option[Seq[PhotoSize]] = None, - sticker: Option[Sticker] = None, - video: Option[Video] = None, - voice: Option[Voice] = None, - video_note: Option[VideoNote] = None, - caption: Option[String] = None, - contact: Option[Contact] = None, - location: Option[Location] = None, - venue: Option[Venue] = None, - new_chat_members: Option[Seq[User]] = None, - left_chat_member: Option[Seq[User]] = None, - new_chat_title: Option[String] = None, - new_chat_photo: Option[Seq[PhotoSize]] = None, - delete_chat_photo: Option[Boolean] = None, - group_chat_created: Option[Boolean] = None, - supergroup_chat_created: Option[Boolean] = None, - channel_chat_created: Option[Boolean] = None, - migrate_to_chat_id: Option[Int] = None, - migrate_from_chat_id: Option[Int] = None, - pinned_message: Option[Message] = None, - invoice: Option[Invoice] = None, - successful_payment: Option[SuccessfulPayment] = None, - connected_website: Option[String] = None - ) - - case class MessageEntity(`type`: String, - offset: Int, - length: Int, - url: Option[String] = None, - user: Option[User] = None - ) - - case class Contact(phone_number: String, - first_name: String, - last_name: Option[String] = None, - user_id: Option[Int] = None - ) - - case class Sticker(file_id: String, - width: Int, - height: Int, - thumb: Option[PhotoSize] = None, - emoji: Option[String] = None, - set_name: Option[String] = None, - mask_position: Option[String] = None, - file_size: Option[Int] = None - ) - - case class Video(file_id: String, - width: Int, - height: Int, - duration: Int, - thumb: Option[PhotoSize] = None, - mime_type: Option[String] = None, - file_size: Option[Int] = None - ) - - case class Audio(file_id: String, - duration: Int, - performer: Option[String] = None, - title: Option[String] = None, - mime_type: Option[String] = None, - file_size: Option[Int] = None - ) - - case class Document(file_id: String, - thumb: Option[PhotoSize] = None, - file_name: Option[String] = None, - mime_type: Option[String] = None, - file_size: Option[Int] = None - ) - - case class PhotoSize(file_id: String, - width: Int, - height: Int, - file_size: Option[Int] = None - ) - - case class Voice(file_id: String, - duration: Int, - mime_type: Option[String] = None, - file_size: Option[Int] = None - ) - - case class VideoNote(file_id: String, - length: Int, - duration: Int, - thumb: Option[PhotoSize] = None, - file_size: Option[Int] = None - ) - - case class ChatPhoto(small_file_id: String, big_file_id: String) - - case class Chat(id: Long, - `type`: String, - title: Option[String] = None, - username: Option[String] = None, - first_name: Option[String] = None, - last_name: Option[String] = None, - all_members_are_administrators: Option[Boolean] = None, - photo: Option[ChatPhoto] = None, - description: Option[String] = None, - invite_link: Option[String] = None, - pinned_message: Option[Message] = None, - sticker_set_name: Option[String] = None, - can_set_sticker_set: Option[Boolean] = None - ) - - case class Game(title: String, - description: String, - photo: Seq[PhotoSize], - text: Option[String] = None, - text_entities: Option[Seq[MessageEntity]] = None, - animation: Option[Animation] = None - ) - - case class Animation(file_id: String, - thumb: Option[PhotoSize] = None, - file_name: Option[String] = None, - mime_type: Option[String] = None, - file_size: Option[Int] = None - ) - - case class InputFile() - - case class Venue(location: Location, - title: String, - address: String, - foursquare_id: Option[String] = None - ) - - case class Invoice(title: String, - description: String, - start_parameter: String, - currency: String, - total_amount: Int - ) - - case class SuccessfulPayment(currency: String, - total_amount: Int, - invoice_payload: String, - shipping_option_id: Option[String] = None, - order_info: Option[OrderInfo] = None, - telegram_payment_charge_id: String, - provider_payment_charge_id: String - ) - - case class Webhook(url: String, - certificate: Option[InputFile] = None, - max_connections: Option[Int] = None, - allowed_updates: Option[Seq[String]] = None - ) - - case class WebhookInfo(url: String, - has_custom_certificate: Boolean, - pending_update_count: Int, - last_error_date: Option[Int] = None, - last_error_message: Option[String] = None, - max_connections: Option[Int] = None, - allowed_updates: Option[Seq[String]] = None - ) -} diff --git a/telegram-bot/src/main/scala/eu/xeppaka/bot/TelegramEntitiesDerivations.scala b/telegram-bot/src/main/scala/eu/xeppaka/bot/TelegramEntitiesDerivations.scala deleted file mode 100644 index acacc1b..0000000 --- a/telegram-bot/src/main/scala/eu/xeppaka/bot/TelegramEntitiesDerivations.scala +++ /dev/null @@ -1,24 +0,0 @@ -package eu.xeppaka.bot - -import cats.syntax.functor._ -import eu.xeppaka.bot.TelegramEntities._ -import io.circe.{Decoder, Encoder} -import io.circe.generic.auto._ -import io.circe.syntax._ - -object TelegramEntitiesDerivations { - implicit val encodeReplyMarkup: Encoder[ReplyMarkup] = Encoder.instance { - case replyKeyboardMarkup: ReplyKeyboardMarkup => replyKeyboardMarkup.asJson - case replyKeyboardRemove: ReplyKeyboardRemove => replyKeyboardRemove.asJson - case inlineKeyboardMarkup: InlineKeyboardMarkup => inlineKeyboardMarkup.asJson - case forceReply: ForceReply => forceReply.asJson - } - - implicit val decodeReplyMarkup: Decoder[ReplyMarkup] = - List[Decoder[ReplyMarkup]]( - Decoder[ReplyKeyboardMarkup].widen, - Decoder[ReplyKeyboardRemove].widen, - Decoder[InlineKeyboardMarkup].widen, - Decoder[ForceReply].widen - ).reduceLeft(_ or _) -}