Skip to content

Commit

Permalink
Add telegram bot UI (#3)
Browse files Browse the repository at this point in the history
  • Loading branch information
mmvpm committed Mar 17, 2024
1 parent 0750e8f commit 2d35354
Show file tree
Hide file tree
Showing 15 changed files with 724 additions and 1 deletion.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@
target
project/target
.bsp
secret
27 changes: 27 additions & 0 deletions bot/src/main/scala/com/github/mmvpm/bot/Main.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.github.mmvpm.bot

import cats.effect.{IO, IOApp}
import com.github.mmvpm.bot.model.MessageID
import com.github.mmvpm.bot.render.RendererImpl
import com.github.mmvpm.bot.state.{State, StateManagerImpl, StorageImpl}
import com.github.mmvpm.bot.util.ResourceUtils
import org.asynchttpclient.Dsl.asyncHttpClient
import sttp.client3.asynchttpclient.cats.AsyncHttpClientCatsBackend

object Main extends IOApp.Simple {

override def run: IO[Unit] =
for {
_ <- IO.println("Starting telegram bot...")

token = ResourceUtils.readTelegramToken()
sttpBackend = AsyncHttpClientCatsBackend.usingClient[IO](asyncHttpClient)
renderer = new RendererImpl
manager = new StateManagerImpl[IO]
stateStorage = new StorageImpl[State](State.Started)
lastMessageStorage = new StorageImpl[Option[MessageID]](None)
bot = new OfferServiceBot[IO](token, sttpBackend, renderer, manager, stateStorage, lastMessageStorage)

_ <- bot.startPolling()
} yield ()
}
87 changes: 87 additions & 0 deletions bot/src/main/scala/com/github/mmvpm/bot/OfferServiceBot.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package com.github.mmvpm.bot

import cats.effect.Concurrent
import cats.implicits.toFlatMapOps
import cats.syntax.functor._
import com.bot4s.telegram.api.declarative.{Callbacks, Command, Commands}
import com.bot4s.telegram.cats.{Polling, TelegramBot}
import com.bot4s.telegram.methods.{EditMessageText, SendDice, SendMessage}
import com.bot4s.telegram.models._
import com.github.mmvpm.bot.model.MessageID
import com.github.mmvpm.bot.render.Renderer
import com.github.mmvpm.bot.state.{State, StateManager, Storage}
import com.github.mmvpm.bot.state.State._
import sttp.client3.SttpBackend

class OfferServiceBot[F[_]: Concurrent](
token: String,
sttpBackend: SttpBackend[F, Any],
renderer: Renderer,
manager: StateManager[F],
stateStorage: Storage[State],
lastMessageStorage: Storage[Option[MessageID]])
extends TelegramBot[F](token, sttpBackend)
with Polling[F]
with Commands[F]
with Callbacks[F] {

// user sent a message (text, image, etc) to the chat
onMessage { implicit message =>
command(message) match {
case Some(Command("roll", _)) => roll
case Some(Command("start", _)) => start
case None =>
getNextStateTag(stateStorage.get) match {
case UnknownTag => fail
case nextTag => replyResolved(nextTag)
}
}
}

// user pressed the button
onCallbackQuery { implicit cq =>
replyResolved(cq.data.get)(cq.message.get)
}

// scenarios

private def roll(implicit message: Message): F[Unit] =
request(SendDice(message.chat.id)).void

private def start(implicit message: Message): F[Unit] =
requestLogged(renderer.render(stateStorage.get, lastMessageStorage.get)).void

private def replyResolved(tag: String)(implicit message: Message): F[Unit] =
for {
nextState <- manager.getNextState(tag, stateStorage.get)
_ = stateStorage.set(withoutError(nextState))
reply = renderer.render(nextState, lastMessageStorage.get)
_ <- requestLogged(reply)
} yield ()

private def fail(implicit message: Message): F[Unit] =
reply("Не понял вас :(").void

// internal

private def withoutError(state: State): State =
state match {
case Error(returnTo, _) => returnTo
case _ => state
}

private def requestLogged(req: Either[EditMessageText, SendMessage]): F[Unit] =
req match {
case Left(toEdit) =>
for {
sent <- request(toEdit)
_ = println(s"Edit $sent")
} yield ()
case Right(toSend) =>
for {
sent <- request(toSend)
_ = lastMessageStorage.set(Some(sent.messageId))(sent)
_ = println(s"Sent $sent")
} yield ()
}
}
7 changes: 7 additions & 0 deletions bot/src/main/scala/com/github/mmvpm/bot/model/Draft.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.github.mmvpm.bot.model

case class Draft(
name: Option[String] = None,
price: Option[Long] = None,
description: Option[String] = None,
photos: Seq[String] = Seq.empty)
8 changes: 8 additions & 0 deletions bot/src/main/scala/com/github/mmvpm/bot/model/package.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.github.mmvpm.bot

package object model {
type ChatID = Long
type MessageID = Int
type Tag = String
type Button = String
}
15 changes: 15 additions & 0 deletions bot/src/main/scala/com/github/mmvpm/bot/render/Renderer.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.github.mmvpm.bot.render

import com.bot4s.telegram.methods.{EditMessageText, SendMessage}
import com.bot4s.telegram.models.Message
import com.github.mmvpm.bot.model.MessageID
import com.github.mmvpm.bot.state.State

trait Renderer {

def render(
state: State,
editMessage: Option[MessageID]
)(implicit message: Message
): Either[EditMessageText, SendMessage]
}
29 changes: 29 additions & 0 deletions bot/src/main/scala/com/github/mmvpm/bot/render/RendererImpl.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.github.mmvpm.bot.render

import com.bot4s.telegram.methods.{EditMessageText, SendMessage}
import com.bot4s.telegram.models.{InlineKeyboardButton, InlineKeyboardMarkup, Message}
import com.github.mmvpm.bot.model.MessageID
import com.github.mmvpm.bot.state.State
import com.github.mmvpm.bot.state.State._

class RendererImpl extends Renderer {

override def render(
state: State,
editMessage: Option[MessageID] = None
)(implicit message: Message): Either[EditMessageText, SendMessage] = {
val buttons = state.next.map { tag =>
InlineKeyboardButton.callbackData(buttonBy(tag), tag)
}
val markup = Some(InlineKeyboardMarkup.singleColumn(buttons))

lazy val send = SendMessage(message.source, state.text, replyMarkup = markup)
lazy val edit = EditMessageText(Some(message.source), editMessage, text = state.text, replyMarkup = markup)

if (editMessage.contains(message.messageId)) // the last message was sent by the bot
Left(edit)
else
Right(send)
}
}

Loading

0 comments on commit 2d35354

Please sign in to comment.