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))) + } +}