diff --git a/.idea/encodings.xml b/.idea/encodings.xml
new file mode 100644
index 0000000..15a15b2
--- /dev/null
+++ b/.idea/encodings.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/.idea/modules/telegram-bot.iml b/.idea/modules/telegram-bot.iml
index 859c125..f2e4e4b 100644
--- a/.idea/modules/telegram-bot.iml
+++ b/.idea/modules/telegram-bot.iml
@@ -6,6 +6,7 @@
+
@@ -14,39 +15,39 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/modules/telegram-bot1.iml b/.idea/modules/telegram-bot1.iml
index 5c2e88b..5a235b4 100644
--- a/.idea/modules/telegram-bot1.iml
+++ b/.idea/modules/telegram-bot1.iml
@@ -11,6 +11,6 @@
-
+
\ No newline at end of file
diff --git a/build.sbt b/build.sbt
index 819586c..5b9baf5 100644
--- a/build.sbt
+++ b/build.sbt
@@ -2,7 +2,7 @@ import Dependencies._
lazy val commonSettings = Seq(
organization := "com.example",
- scalaVersion := "2.12.7",
+ scalaVersion := "2.12.8",
version := "0.1.0-SNAPSHOT",
mainClass := Some("eu.xeppaka.bot.Main")
)
diff --git a/project/Dependencies.scala b/project/Dependencies.scala
index f0df78a..cc65768 100644
--- a/project/Dependencies.scala
+++ b/project/Dependencies.scala
@@ -1,11 +1,11 @@
import sbt._
object Dependencies {
- lazy val akka = "com.typesafe.akka" %% "akka-actor" % "2.5.17"
- lazy val akkaTyped = "com.typesafe.akka" %% "akka-actor-typed" % "2.5.17"
- lazy val akkaStream = "com.typesafe.akka" %% "akka-stream" % "2.5.17"
+ 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.17"
+ 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"
diff --git a/project/build.properties b/project/build.properties
index 7c58a83..72f9028 100644
--- a/project/build.properties
+++ b/project/build.properties
@@ -1 +1 @@
-sbt.version=1.2.6
+sbt.version=1.2.7
diff --git a/telegram-bot/src/main/scala/eu/xeppaka/bot/CheckDeliveryDialog.scala b/telegram-bot/src/main/scala/eu/xeppaka/bot/CheckDeliveryDialog.scala
index 11770ae..553ee28 100644
--- a/telegram-bot/src/main/scala/eu/xeppaka/bot/CheckDeliveryDialog.scala
+++ b/telegram-bot/src/main/scala/eu/xeppaka/bot/CheckDeliveryDialog.scala
@@ -2,11 +2,13 @@ package eu.xeppaka.bot
import akka.actor.typed.scaladsl.adapter._
import akka.actor.typed.scaladsl.{Behaviors, StashBuffer}
-import akka.actor.typed.{ActorRef, Behavior, DispatcherSelector}
+import akka.actor.typed.{ActorRef, Behavior, DispatcherSelector, SupervisorStrategy}
import akka.http.scaladsl.Http
import akka.http.scaladsl.model._
import akka.util.{ByteString, Timeout}
-import eu.xeppaka.bot.TelegramEntities.{Message, SendMessage}
+import eu.xeppaka.bot.TelegramEntities._
+import eu.xeppaka.bot.TelegramEntitiesDerivations._
+import io.circe.Printer
import scala.concurrent.ExecutionContext
import scala.concurrent.duration._
@@ -27,46 +29,63 @@ object CheckDeliveryDialog {
case object Help extends DialogCommand
object DialogCommand {
- def apply(msg: String, replyTo: ActorRef[CommandResult]): Option[DialogCommand] = msg match {
- case "/add" => Some(AddParcel)
- case "/remove" => Some(RemoveParcel)
- case "/list" => Some(ListParcels)
- case "/help" => Some(Help)
- case _ => None
+ 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 replyKeyboardRemoveMarkup = Some(ReplyKeyboardRemove())
def behavior(chatId: Long, botUri: BotUri): Behavior[Command] = Behaviors.setup[Command] { ctx =>
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(CzechPostDeliveryCheck.behavior(chatId.toString, deliveryStateAdapter))
+ val czechPostDeliveryCheck = ctx.spawnAnonymous(Behaviors.supervise(CzechPostDeliveryCheck.behavior(chatId.toString, deliveryStateAdapter)).onFailure(SupervisorStrategy.restart))
- def initial: Behavior[Command] = Behaviors.receiveMessage {
+ def initial: Behavior[Command] = waitCommand
+
+ def waitCommand: Behavior[Command] = Behaviors.receiveMessage {
case ProcessMessage(msg, replyTo) =>
- val command = DialogCommand(msg.text.getOrElse("unknown message"), replyTo)
+ val command = msg.text.map(text => DialogCommand.parse(text))
replyTo ! ProcessMessageSuccess
if (command.isDefined) {
ctx.self ! command.get
Behaviors.same
} else {
- sendMessage("This command is unsupported.", initial, initial)
+ val message = SendMessage(chatId, "This command is unsupported.")
+ sendMessage(message, initial, initial)
}
case AddParcel =>
- sendMessage("Please enter parcel ID.", waitParcelId(parcelId => addParcel(parcelId)), initial)
+ val message = SendMessage(chatId, "Please enter a parcel ID.")
+ sendMessage(message, waitParcelId(parcelId => addParcel(parcelId)), initial)
case RemoveParcel =>
- sendMessage("Please enter parcel ID.", waitParcelId(parcelId => removeParcel(parcelId)), initial)
+ removeParcel(initial, initial)
case ListParcels =>
listParcels
case Help =>
- sendMessage("Supported commands: /add, /remove, /list, /help", initial, initial)
+ val message = SendMessage(chatId, helpMessage)
+ sendMessage(message, initial, initial)
case DeliveryStateChanged(state) =>
- sendMessage(state, initial, initial)
+ val message = SendMessage(chatId, state, Some("Markdown"))
+ sendMessage(message, initial, initial)
case _ =>
Behaviors.unhandled
}
@@ -84,14 +103,17 @@ object CheckDeliveryDialog {
Behaviors.receiveMessage {
case AddParcelSuccess =>
- sendMessage(s"Parcel $parcelId was added to the watch list.", initial, initial)
+ 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(_) =>
- sendMessage(s"Parcel $parcelId is in the watch list already.", initial, initial)
+ 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")
- sendMessage(s"Adding parcel failed. Please try again.", initial, initial)
+ val message = SendMessage(chatId, s"Adding parcel failed. Please try again.")
+ sendMessage(message, initial, initial)
}
case otherMessage =>
stashBuffer.stash(otherMessage)
@@ -100,7 +122,7 @@ object CheckDeliveryDialog {
}
def listParcels: Behavior[Command] = Behaviors.setup { ctx =>
- case class ListParcelsSuccess(parcelsList: String) extends Command
+ case class ListParcelsSuccess(parcelsList: Set[String]) extends Command
case class ListParcelsFailure(exception: Throwable) extends Command
implicit val timeout: Timeout = 5.seconds
@@ -111,17 +133,52 @@ object CheckDeliveryDialog {
Behaviors.receiveMessage {
case ListParcelsSuccess(parcelsList) =>
- sendMessage(parcelsList, initial, initial)
+ 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)
- sendMessage("Failed to get list of the your watched parcels. Please try again later.", initial, initial)
+ 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(parcelId: String): Behavior[Command] = Behaviors.setup { ctx =>
+ 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))
+ 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
@@ -134,14 +191,17 @@ object CheckDeliveryDialog {
Behaviors.receiveMessage {
case RemoveParcelSuccess =>
- sendMessage(s"Parcel $parcelId was removed from the watch list.", initial, initial)
+ val message = SendMessage(chatId, s"Parcel $parcelId was removed from the watch list.", reply_markup = replyKeyboardRemoveMarkup)
+ sendMessage(message, initial, initial)
case RemoveParcelFailure(exception) =>
exception match {
case CzechPostDeliveryCheck.ParcelIdNotFound(_) =>
- sendMessage(s"Parcel $parcelId is not found in the list of the watched parcels.", initial, initial)
+ val message = SendMessage(chatId, s"Parcel $parcelId is not found in the list of the watched parcels.", reply_markup = replyKeyboardRemoveMarkup)
+ sendMessage(message, initial, initial)
case _ =>
ctx.log.error(exception, "action=add_parcel result=failure")
- sendMessage(s"Remove of the parcel failed. Please try again.", initial, initial)
+ val message = SendMessage(chatId, s"Remove of the parcel failed. Please try again.", reply_markup = replyKeyboardRemoveMarkup)
+ sendMessage(message, initial, initial)
}
case otherMessage =>
stashBuffer.stash(otherMessage)
@@ -149,52 +209,68 @@ object CheckDeliveryDialog {
}
}
- def waitParcelId(onSuccess: String => Behavior[Command]): Behavior[Command] = Behaviors.setup[Command] { ctx =>
- Behaviors.receiveMessage {
- case ProcessMessage(msg, replyTo) =>
- if (msg.text.isDefined) {
- val parcelId = msg.text.get
- replyTo ! ProcessMessageSuccess
- onSuccess(parcelId)
- } else {
- replyTo ! ProcessMessageSuccess
- waitParcelId(onSuccess)
- }
- case otherMsg =>
- stashBuffer.stash(otherMsg)
- 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(text: String, onSuccess: => Behavior[Command], onFailure: => Behavior[Command]): Behavior[Command] = Behaviors.setup[Command] { ctx =>
- import io.circe._
+ 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 sendMessage = SendMessage(chatId, text, Some("Markdown"))
- val printer = Printer.noSpaces.copy(dropNullValues = true)
- val json = printer.pretty(sendMessage.asJson)
+ 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)
+
http
.singleRequest(request)
.onComplete {
case Success(response) => if (response.status.isSuccess()) {
+ ctx.log.debug("action=send_message status=finished result=success chat_id={}", chatId)
ctx.self ! SendMessageSuccess
} else {
+ ctx.log.error("action=send_message status=finished result=failure chat_id={} http_code={}", chatId, response.status.value)
ctx.self ! SendMessageFailure(new RuntimeException(s"Error while sending message. HTTP status: ${response.status}."))
}
- case Failure(exception) => ctx.self ! SendMessageFailure(exception)
+ case Failure(exception) =>
+ ctx.log.error(exception, "action=send_message status=finished result=failure chat_id={}", chatId)
+ ctx.self ! SendMessageFailure(exception)
}
Behaviors.receiveMessage {
case SendMessageSuccess =>
stashBuffer.unstashAll(ctx, onSuccess)
case SendMessageFailure(exception) =>
- ctx.log.error(exception, "action=send_message result=failure")
- stashBuffer.unstashAll(ctx, onFailure)
+ if (attempt >= 5) {
+ ctx.log.error(exception, "action=send_message result=failure")
+ stashBuffer.unstashAll(ctx, onFailure)
+ } else {
+ sendMessage(message, onSuccess, onFailure, attempt + 1)
+ }
case otherMsg =>
stashBuffer.stash(otherMsg)
Behaviors.same
diff --git a/telegram-bot/src/main/scala/eu/xeppaka/bot/CzechPostDeliveryCheck.scala b/telegram-bot/src/main/scala/eu/xeppaka/bot/CzechPostDeliveryCheck.scala
index b8e1af7..4a99c9b 100644
--- a/telegram-bot/src/main/scala/eu/xeppaka/bot/CzechPostDeliveryCheck.scala
+++ b/telegram-bot/src/main/scala/eu/xeppaka/bot/CzechPostDeliveryCheck.scala
@@ -1,6 +1,7 @@
package eu.xeppaka.bot
import java.security.cert.X509Certificate
+import java.text.SimpleDateFormat
import akka.actor.ActorSystem
import akka.actor.typed.scaladsl.adapter._
@@ -12,8 +13,9 @@ 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.scaladsl.PersistentBehaviors.{CommandHandler, EventHandler}
-import akka.persistence.typed.scaladsl.{Effect, PersistentBehaviors}
+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._
@@ -26,6 +28,7 @@ import scala.concurrent.duration._
import scala.util.{Failure, Success}
object Entities {
+
case class Attributes(
parcelType: String,
weight: Double,
@@ -43,10 +46,7 @@ object Entities {
latitude: Option[Double],
longitude: Option[Double],
timeDeliveryAttempt: Option[String]
- ) {
- def prettyPrint: String =
- s"$date\n$text"
- }
+ )
case class States(state: Seq[State])
@@ -54,18 +54,23 @@ object Entities {
}
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
- .map(state => s"${state.prettyPrint}\n===========================\n")
+ .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
+ |===========================
+ |$statesString""".stripMargin
}
}
case class State(parcelStates: Map[String, ParcelState] = Map.empty)
@@ -73,7 +78,7 @@ object CzechPostDeliveryCheck {
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: String)
+ case class ListParcelsResult(parcelsList: Set[String])
case object CommandResultSuccess extends CommandResult
case class CommandResultFailure(exception: Throwable) extends CommandResult
@@ -133,31 +138,33 @@ object CzechPostDeliveryCheck {
val commandHandler: CommandHandler[Command, Event, State] = (state, cmd) => {
cmd match {
case AddParcel(parcelId, replyTo) =>
- if (state.parcelStates.keySet.contains(parcelId)) {
+ val parcelIdUpper = parcelId.toUpperCase
+ if (state.parcelStates.keySet.contains(parcelIdUpper)) {
Effect
.none
- .thenRun(_ => replyTo ! CommandResultFailure(DuplicateParcelId(parcelId)))
+ .thenRun(_ => replyTo ! CommandResultFailure(DuplicateParcelId(parcelIdUpper)))
} else {
Effect
- .persist(ParcelAdded(parcelId))
+ .persist(ParcelAdded(parcelIdUpper))
.thenRun(_ => {
replyTo ! CommandResultSuccess
ctx.self ! CheckParcels
})
}
case RemoveParcel(parcelId, replyTo) =>
- if (state.parcelStates.keySet.contains(parcelId)) {
+ val parcelIdUpper = parcelId.toUpperCase
+ if (state.parcelStates.keySet.contains(parcelIdUpper)) {
Effect
- .persist(ParcelRemoved(parcelId))
+ .persist(ParcelRemoved(parcelIdUpper))
.thenRun(_ => replyTo ! CommandResultSuccess)
} else {
Effect
.none
- .thenRun(_ => replyTo ! CommandResultFailure(ParcelIdNotFound(parcelId)))
+ .thenRun(_ => replyTo ! CommandResultFailure(ParcelIdNotFound(parcelIdUpper)))
}
case ListParcels(replyTo) =>
- val parcelsList = "*List of your watched parcels:*\n" + (if (state.parcelStates.keys.nonEmpty) state.parcelStates.keys.toSeq.sorted.map(id => id + "\n").mkString else "(empty)")
+ val parcelsList = state.parcelStates.keySet
Effect.none
.thenRun(_ => replyTo ! ListParcelsResult(parcelsList))
@@ -229,8 +236,8 @@ object CzechPostDeliveryCheck {
}
}
- PersistentBehaviors.receive[Command, Event, State](
- persistenceId = s"$chatId-czechpost",
+ 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/Main.scala b/telegram-bot/src/main/scala/eu/xeppaka/bot/Main.scala
index 6dcf49e..f745493 100644
--- a/telegram-bot/src/main/scala/eu/xeppaka/bot/Main.scala
+++ b/telegram-bot/src/main/scala/eu/xeppaka/bot/Main.scala
@@ -14,8 +14,8 @@ import scala.io.StdIn
object Main {
def main(args: Array[String]): Unit = {
- //val botId = "570855144:AAEv7b817cuq2JJI9f2kG5B9G3zW1x-btz4" // useless bot
- val botId = "693134480:AAE8JRXA6j1mkOKTaxapP6A-E4LPHRuiIf8" // delivery bot
+ val botId = "570855144:AAEv7b817cuq2JJI9f2kG5B9G3zW1x-btz4" // useless bot
+ //val botId = "693134480:AAE8JRXA6j1mkOKTaxapP6A-E4LPHRuiIf8" // delivery bot
val telegramBot = ActorSystem(TelegramBot.behavior(botId, "0.0.0.0", 8443), "telegram-bot")
implicit val actorSystem: actor.ActorSystem = telegramBot.toUntyped
implicit val executionContext: ExecutionContextExecutor = telegramBot.dispatchers.lookup(DispatcherSelector.default())
diff --git a/telegram-bot/src/main/scala/eu/xeppaka/bot/PostType.scala b/telegram-bot/src/main/scala/eu/xeppaka/bot/PostType.scala
new file mode 100644
index 0000000..ae47a25
--- /dev/null
+++ b/telegram-bot/src/main/scala/eu/xeppaka/bot/PostType.scala
@@ -0,0 +1,8 @@
+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/TelegramEntities.scala b/telegram-bot/src/main/scala/eu/xeppaka/bot/TelegramEntities.scala
index ab85743..947193c 100644
--- a/telegram-bot/src/main/scala/eu/xeppaka/bot/TelegramEntities.scala
+++ b/telegram-bot/src/main/scala/eu/xeppaka/bot/TelegramEntities.scala
@@ -10,6 +10,35 @@ object TelegramEntities {
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,
@@ -92,7 +121,7 @@ object TelegramEntities {
disable_web_page_preview: Option[Boolean] = None,
disable_notification: Option[Boolean] = None,
reply_to_message_id: Option[Int] = None,
- reply_markup: Option[String] = None
+ reply_markup: Option[ReplyMarkup] = None
)
case class Message(message_id: Int,
diff --git a/telegram-bot/src/main/scala/eu/xeppaka/bot/TelegramEntitiesDerivations.scala b/telegram-bot/src/main/scala/eu/xeppaka/bot/TelegramEntitiesDerivations.scala
new file mode 100644
index 0000000..acacc1b
--- /dev/null
+++ b/telegram-bot/src/main/scala/eu/xeppaka/bot/TelegramEntitiesDerivations.scala
@@ -0,0 +1,24 @@
+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 _)
+}
diff --git a/telegram-bot/src/test/scala/eu/xeppaka/bot/JsonSpec.scala b/telegram-bot/src/test/scala/eu/xeppaka/bot/JsonSpec.scala
new file mode 100644
index 0000000..f70d725
--- /dev/null
+++ b/telegram-bot/src/test/scala/eu/xeppaka/bot/JsonSpec.scala
@@ -0,0 +1,16 @@
+package eu.xeppaka.bot
+
+import eu.xeppaka.bot.TelegramEntities._
+import io.circe.Printer
+import io.circe.generic.auto._
+import io.circe.syntax._
+import org.scalatest.FlatSpec
+import TelegramEntitiesDerivations._
+
+class JsonSpec extends FlatSpec {
+ "blah" should "blah" in {
+ val keyboard = ReplyKeyboardRemove()
+ val message = SendMessage(100000, "Please enter command.", reply_markup = Some(keyboard))
+ println(message.asJson.pretty(Printer.spaces2.copy(dropNullValues = true)))
+ }
+}