diff --git a/telegram-bot/src/main/scala/eu/xeppaka/bot/BotUri.scala b/telegram-bot/src/main/scala/eu/xeppaka/bot/BotUri.scala index 82040c0..100fddd 100644 --- a/telegram-bot/src/main/scala/eu/xeppaka/bot/BotUri.scala +++ b/telegram-bot/src/main/scala/eu/xeppaka/bot/BotUri.scala @@ -11,4 +11,5 @@ case class BotUri(botId: String) { 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 index 553ee28..e1a5713 100644 --- a/telegram-bot/src/main/scala/eu/xeppaka/bot/CheckDeliveryDialog.scala +++ b/telegram-bot/src/main/scala/eu/xeppaka/bot/CheckDeliveryDialog.scala @@ -5,6 +5,8 @@ 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._ @@ -50,16 +52,21 @@ object CheckDeliveryDialog { |/list - list watched parcels |/remove - remove parcel from a watching list """.stripMargin - private val replyKeyboardRemoveMarkup = Some(ReplyKeyboardRemove()) + 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] = waitCommand + 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) => @@ -161,7 +168,7 @@ object CheckDeliveryDialog { 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 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 { @@ -191,16 +198,16 @@ object CheckDeliveryDialog { Behaviors.receiveMessage { case RemoveParcelSuccess => - val message = SendMessage(chatId, s"Parcel $parcelId was removed from the watch list.", reply_markup = replyKeyboardRemoveMarkup) + 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.", reply_markup = replyKeyboardRemoveMarkup) + 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.", reply_markup = replyKeyboardRemoveMarkup) + val message = SendMessage(chatId, s"Remove of the parcel failed. Please try again.") sendMessage(message, initial, initial) } case otherMessage => @@ -209,15 +216,15 @@ object CheckDeliveryDialog { } } -// 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 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) => @@ -246,27 +253,36 @@ object CheckDeliveryDialog { 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.log.error(exception, "action=send_message status=finished result=failure chat_id={}", chatId) - ctx.self ! SendMessageFailure(exception) + 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") + ctx.log.error(exception, "action=send_message result=failure message=attempts threshold exceeded") stashBuffer.unstashAll(ctx, onFailure) } else { sendMessage(message, onSuccess, onFailure, attempt + 1) 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 947193c..b2075e0 100644 --- a/telegram-bot/src/main/scala/eu/xeppaka/bot/TelegramEntities.scala +++ b/telegram-bot/src/main/scala/eu/xeppaka/bot/TelegramEntities.scala @@ -115,6 +115,12 @@ object TelegramEntities { 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,