Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add telegram bot UI (#3) #4

Merged
merged 2 commits into from
Mar 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 ()
}
}
8 changes: 8 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,8 @@
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
}
14 changes: 14 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,14 @@
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]
}
28 changes: 28 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,28 @@
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
Loading