Skip to content

Commit

Permalink
Add auth to telegram bot (#13)
Browse files Browse the repository at this point in the history
  • Loading branch information
mmvpm committed Mar 31, 2024
1 parent e6c4307 commit 9a53987
Show file tree
Hide file tree
Showing 23 changed files with 405 additions and 20 deletions.
4 changes: 4 additions & 0 deletions bot/src/main/resources/application.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
ofs {
base-url = "http://localhost:8080"
request-timeout = 20s
}
7 changes: 7 additions & 0 deletions bot/src/main/scala/com/github/mmvpm/bot/Config.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.github.mmvpm.bot

import scala.concurrent.duration.FiniteDuration

case class Config(ofs: OfsConfig)

case class OfsConfig(baseUrl: String, requestTimeout: FiniteDuration)
27 changes: 21 additions & 6 deletions bot/src/main/scala/com/github/mmvpm/bot/Main.scala
Original file line number Diff line number Diff line change
@@ -1,26 +1,41 @@
package com.github.mmvpm.bot

import cats.effect.std.Random
import cats.effect.{IO, IOApp}
import com.github.mmvpm.bot.client.ofs.{OfsClient, OfsClientSttp}
import com.github.mmvpm.bot.manager.ofs.{OfsManager, OfsManagerImpl}
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.render.{Renderer, RendererImpl}
import com.github.mmvpm.bot.state.{State, StateManager, StateManagerImpl, StorageImpl}
import com.github.mmvpm.bot.util.ResourceUtils
import com.github.mmvpm.model.Session
import org.asynchttpclient.Dsl.asyncHttpClient
import pureconfig.ConfigSource
import pureconfig.generic.auto._
import sttp.client3.asynchttpclient.cats.AsyncHttpClientCatsBackend

object Main extends IOApp.Simple {

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

token = ResourceUtils.readTelegramToken()
config = ConfigSource.default.loadOrThrow[Config]

sttpBackend = AsyncHttpClientCatsBackend.usingClient[IO](asyncHttpClient)
renderer = new RendererImpl
manager = new StateManagerImpl[IO]

stateStorage = new StorageImpl[State](State.Started)
sessionStorage = new StorageImpl[Option[Session]](None)
lastMessageStorage = new StorageImpl[Option[MessageID]](None)
bot = new OfferServiceBot[IO](token, sttpBackend, renderer, manager, stateStorage, lastMessageStorage)

ofsClient: OfsClient[IO] = new OfsClientSttp[IO](config.ofs, sttpBackend)
ofsManager: OfsManager[IO] = new OfsManagerImpl[IO](ofsClient, sessionStorage, random)

renderer: Renderer = new RendererImpl
manager: StateManager[IO] = new StateManagerImpl[IO](ofsManager)

bot = new OfferServiceBot[IO](token, sttpBackend, renderer, manager, stateStorage, lastMessageStorage, ofsManager)

_ <- bot.startPolling()
} yield ()
Expand Down
30 changes: 24 additions & 6 deletions bot/src/main/scala/com/github/mmvpm/bot/OfferServiceBot.scala
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ 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.manager.ofs.OfsManager
import com.github.mmvpm.bot.manager.ofs.error.OfsError.InvalidSession
import com.github.mmvpm.bot.manager.ofs.response.LoginOrRegisterResponse
import com.github.mmvpm.bot.model.MessageID
import com.github.mmvpm.bot.render.Renderer
import com.github.mmvpm.bot.state.{State, StateManager, Storage}
Expand All @@ -17,9 +20,10 @@ class OfferServiceBot[F[_]: Concurrent](
token: String,
sttpBackend: SttpBackend[F, Any],
renderer: Renderer,
manager: StateManager[F],
stateManager: StateManager[F],
stateStorage: Storage[State],
lastMessageStorage: Storage[Option[MessageID]]
lastMessageStorage: Storage[Option[MessageID]],
ofsManager: OfsManager[F]
) extends TelegramBot[F](token, sttpBackend)
with Polling[F]
with Commands[F]
Expand Down Expand Up @@ -50,11 +54,25 @@ class OfferServiceBot[F[_]: Concurrent](
request(SendDice(message.chat.id)).void

private def start(implicit message: Message): F[Unit] =
requestLogged(renderer.render(stateStorage.get, lastMessageStorage.get)).void
ofsManager.loginOrRegister.value.flatMap {
case Left(InvalidSession) =>
val state: State = EnterPassword
stateStorage.set(state)
requestLogged(renderer.render(state, None))
case Left(error) =>
reply(s"Ошибка: ${error.details}. Попробуйте команду /start ещё раз").void
case Right(response) =>
val state = response match {
case LoginOrRegisterResponse.LoggedIn(name) => LoggedIn(name)
case LoginOrRegisterResponse.Registered(password) => Registered(password)
}
val saveMessageId = !state.isInstanceOf[Registered] // to save password in the chat
requestLogged(renderer.render(state, None), saveMessageId)
}

private def replyResolved(tag: String)(implicit message: Message): F[Unit] =
for {
nextState <- manager.getNextState(tag, stateStorage.get)
nextState <- stateManager.getNextState(tag, stateStorage.get)
_ = stateStorage.set(withoutError(nextState))
reply = renderer.render(nextState, lastMessageStorage.get)
_ <- requestLogged(reply)
Expand All @@ -71,7 +89,7 @@ class OfferServiceBot[F[_]: Concurrent](
case _ => state
}

private def requestLogged(req: Either[EditMessageText, SendMessage]): F[Unit] =
private def requestLogged(req: Either[EditMessageText, SendMessage], saveMessageId: Boolean = true): F[Unit] =
req match {
case Left(toEdit) =>
for {
Expand All @@ -81,7 +99,7 @@ class OfferServiceBot[F[_]: Concurrent](
case Right(toSend) =>
for {
sent <- request(toSend)
_ = lastMessageStorage.set(Some(sent.messageId))(sent)
_ = if (saveMessageId) lastMessageStorage.set(Some(sent.messageId))(sent)
_ = println(s"Sent $sent")
} yield ()
}
Expand Down
15 changes: 15 additions & 0 deletions bot/src/main/scala/com/github/mmvpm/bot/client/ofs/OfsClient.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.github.mmvpm.bot.client.ofs

import cats.data.EitherT
import com.github.mmvpm.model.{OfferDescription, Session}
import com.github.mmvpm.bot.client.ofs.error.OfsClientError
import com.github.mmvpm.bot.client.ofs.response.{CreateOfferResponse, SignInResponse, SignUpResponse, UserIdResponse}

trait OfsClient[F[_]] {

def signUp(name: String, login: String, password: String): EitherT[F, OfsClientError, SignUpResponse]
def signIn(login: String, password: String): EitherT[F, OfsClientError, SignInResponse]
def whoami(session: Session): EitherT[F, OfsClientError, UserIdResponse]

def createOffer(session: Session, description: OfferDescription): EitherT[F, OfsClientError, CreateOfferResponse]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package com.github.mmvpm.bot.client.ofs

import cats.data.EitherT
import cats.MonadThrow
import cats.implicits.{toBifunctorOps, toFunctorOps}
import com.github.mmvpm.model.{OfferDescription, Session}
import com.github.mmvpm.bot.OfsConfig
import com.github.mmvpm.bot.client.ofs.request._
import com.github.mmvpm.bot.client.ofs.response._
import com.github.mmvpm.bot.client.ofs.error._
import io.circe.generic.auto._
import io.circe.Error
import sttp.client3._
import sttp.client3.circe._

class OfsClientSttp[F[_]: MonadThrow](ofsConfig: OfsConfig, sttpBackend: SttpBackend[F, Any]) extends OfsClient[F] {

def signUp(name: String, login: String, password: String): EitherT[F, OfsClientError, SignUpResponse] = {
val requestUri = uri"${ofsConfig.baseUrl}/api/v1/auth/sign-up"
val request = SignUpRequest(name, login, password)

val response = basicRequest
.post(requestUri)
.body(request)
.response(asJsonEither[OfsApiClientError, SignUpResponse])
.readTimeout(ofsConfig.requestTimeout)
.send(sttpBackend)
.map(_.body.leftMap(parseFailure))

EitherT(response)
}

override def signIn(login: String, password: String): EitherT[F, OfsClientError, SignInResponse] = {
val requestUri = uri"${ofsConfig.baseUrl}/api/v1/auth/sign-in"

val response = basicRequest
.post(requestUri)
.auth
.basic(login, password)
.response(asJsonEither[OfsApiClientError, SignInResponse])
.readTimeout(ofsConfig.requestTimeout)
.send(sttpBackend)
.map(_.body.leftMap(parseFailure))

EitherT(response)
}

override def createOffer(
session: Session,
description: OfferDescription
): EitherT[F, OfsClientError, CreateOfferResponse] = {
val requestUri = uri"${ofsConfig.baseUrl}/api/v1/offer"
val request = CreateOfferRequest(description)

val response = basicRequest
.post(requestUri)
.body(request)
.header(SessionHeaderName, session.toString)
.response(asJsonEither[OfsApiClientError, CreateOfferResponse])
.readTimeout(ofsConfig.requestTimeout)
.send(sttpBackend)
.map(_.body.leftMap(parseFailure))

EitherT(response)
}

def whoami(session: Session): EitherT[F, OfsClientError, UserIdResponse] = {
val requestUri = uri"${ofsConfig.baseUrl}/api/v1/auth/whoami/$session"

val response = basicRequest
.get(requestUri)
.response(asJsonEither[OfsApiClientError, UserIdResponse])
.readTimeout(ofsConfig.requestTimeout)
.send(sttpBackend)
.map(_.body.leftMap(parseFailure))

EitherT(response)
}

// internal

private def parseFailure: ResponseException[OfsApiClientError, Error] => OfsClientError = {
case HttpError(body, _) => body
case error => OfsUnknownClientError(error.getMessage)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.github.mmvpm.bot.client.ofs

package object error {

trait OfsClientError {
def details: String
}

case class OfsApiClientError(id: String, code: Int, details: String) extends OfsClientError

case class OfsUnknownClientError(details: String) extends OfsClientError
}
5 changes: 5 additions & 0 deletions bot/src/main/scala/com/github/mmvpm/bot/client/ofs/ofs.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.github.mmvpm.bot.client

package object ofs {
val SessionHeaderName = "X-Auth-Session"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.github.mmvpm.bot.client.ofs.request

import com.github.mmvpm.model.OfferDescription

case class CreateOfferRequest(description: OfferDescription)
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package com.github.mmvpm.bot.client.ofs.request

case class SignUpRequest(name: String, login: String, password: String)
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.github.mmvpm.bot.client.ofs.request

import com.github.mmvpm.model.Session

case class WhoamiRequest(session: Session)
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.github.mmvpm.bot.client.ofs.response

import com.github.mmvpm.model.OfferID

case class CreateOfferResponse(offer: OfsOffer)

case class OfsOffer(id: OfferID) // it's not necessary to copy all fields
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.github.mmvpm.bot.client.ofs.response

import com.github.mmvpm.model.Session

case class SignInResponse(session: Session)
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.github.mmvpm.bot.client.ofs.response

case class SignUpResponse(user: OfsUser)

case class OfsUser(id: String) // not necessary to copy all fields
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.github.mmvpm.bot.client.ofs.response

import com.github.mmvpm.model.UserID

case class UserIdResponse(userId: UserID)
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.github.mmvpm.bot.manager.ofs

import cats.data.EitherT
import com.bot4s.telegram.models.{Chat, Message}
import com.github.mmvpm.bot.manager.ofs.error.OfsError
import com.github.mmvpm.bot.manager.ofs.response.LoginOrRegisterResponse
import com.github.mmvpm.model.OfferDescription

trait OfsManager[F[_]] {
def login(implicit message: Message): EitherT[F, OfsError, LoginOrRegisterResponse.LoggedIn]
def loginOrRegister(implicit message: Message): EitherT[F, OfsError, LoginOrRegisterResponse]
def createOffer(description: OfferDescription)(implicit message: Message): EitherT[F, OfsError, Unit]
}
Loading

0 comments on commit 9a53987

Please sign in to comment.