Skip to content

Commit

Permalink
Merge pull request #21 from mmvpm/connect_telegram_bot_with_backend
Browse files Browse the repository at this point in the history
Connect telegram bot with backend (#12)
  • Loading branch information
mmvpm authored Apr 1, 2024
2 parents 921a9ab + 4196c95 commit 5d1d26a
Show file tree
Hide file tree
Showing 24 changed files with 467 additions and 97 deletions.
2 changes: 1 addition & 1 deletion bot/src/main/resources/application.conf
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
ofs {
base-url = "http://localhost:8080"
request-timeout = 20s
request-timeout = 5s
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
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}
import com.github.mmvpm.bot.client.ofs.response._
import com.github.mmvpm.bot.model.{Draft, OfferPatch}
import com.github.mmvpm.model.{OfferDescription, OfferID, Session}

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 getOffer(offerId: OfferID): EitherT[F, OfsClientError, OfferResponse]
def getOffers(offerIds: Seq[OfferID]): EitherT[F, OfsClientError, OffersResponse]
def getMyOffers(session: Session): EitherT[F, OfsClientError, OffersResponse]
def createOffer(session: Session, description: OfferDescription): EitherT[F, OfsClientError, CreateOfferResponse]
def updateOffer(session: Session, offerId: OfferID, patch: OfferPatch): EitherT[F, OfsClientError, OfferResponse]
def deleteOffer(session: Session, offerId: OfferID): EitherT[F, OfsClientError, OkResponse]
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@ 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 cats.implicits.{catsSyntaxApplicativeError, toBifunctorOps, toFunctorOps}
import com.github.mmvpm.model.{OfferDescription, OfferID, 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 com.github.mmvpm.bot.client.ofs.util.CirceInstances._
import com.github.mmvpm.bot.model.{Draft, OfferPatch}
import io.circe.Error
import sttp.client3._
import sttp.client3.circe._
Expand All @@ -26,11 +28,12 @@ class OfsClientSttp[F[_]: MonadThrow](ofsConfig: OfsConfig, sttpBackend: SttpBac
.readTimeout(ofsConfig.requestTimeout)
.send(sttpBackend)
.map(_.body.leftMap(parseFailure))
.recover(error => Left(OfsUnknownClientError(error.getMessage)))

EitherT(response)
}

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

val response = basicRequest
Expand All @@ -41,11 +44,70 @@ class OfsClientSttp[F[_]: MonadThrow](ofsConfig: OfsConfig, sttpBackend: SttpBac
.readTimeout(ofsConfig.requestTimeout)
.send(sttpBackend)
.map(_.body.leftMap(parseFailure))
.recover(error => Left(OfsUnknownClientError(error.getMessage)))

EitherT(response)
}

override def createOffer(
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))
.recover(error => Left(OfsUnknownClientError(error.getMessage)))

EitherT(response)
}

def getOffer(offerId: OfferID): EitherT[F, OfsClientError, OfferResponse] = {
val requestUri = uri"${ofsConfig.baseUrl}/api/v1/offer/$offerId"

val response = basicRequest
.get(requestUri)
.response(asJsonEither[OfsApiClientError, OfferResponse])
.readTimeout(ofsConfig.requestTimeout)
.send(sttpBackend)
.map(_.body.leftMap(parseFailure))
.recover(error => Left(OfsUnknownClientError(error.getMessage)))

EitherT(response)
}

def getOffers(offerIds: Seq[OfferID]): EitherT[F, OfsClientError, OffersResponse] = {
val requestUri = uri"${ofsConfig.baseUrl}/api/v1/offer/list"

val response = basicRequest
.post(requestUri)
.body(GetOffersRequest(offerIds.toList))
.response(asJsonEither[OfsApiClientError, OffersResponse])
.readTimeout(ofsConfig.requestTimeout)
.send(sttpBackend)
.map(_.body.leftMap(parseFailure))
.recover(error => Left(OfsUnknownClientError(error.getMessage)))

EitherT(response)
}

def getMyOffers(session: Session): EitherT[F, OfsClientError, OffersResponse] = {
val requestUri = uri"${ofsConfig.baseUrl}/api/v1/offer/list/my"

val response = basicRequest
.get(requestUri)
.header(SessionHeaderName, session.toString)
.response(asJsonEither[OfsApiClientError, OffersResponse])
.readTimeout(ofsConfig.requestTimeout)
.send(sttpBackend)
.map(_.body.leftMap(parseFailure))
.recover(error => Left(OfsUnknownClientError(error.getMessage)))

EitherT(response)
}

def createOffer(
session: Session,
description: OfferDescription
): EitherT[F, OfsClientError, CreateOfferResponse] = {
Expand All @@ -60,19 +122,38 @@ class OfsClientSttp[F[_]: MonadThrow](ofsConfig: OfsConfig, sttpBackend: SttpBac
.readTimeout(ofsConfig.requestTimeout)
.send(sttpBackend)
.map(_.body.leftMap(parseFailure))
.recover(error => Left(OfsUnknownClientError(error.getMessage)))

EitherT(response)
}

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

val response = basicRequest
.get(requestUri)
.response(asJsonEither[OfsApiClientError, UserIdResponse])
.put(requestUri)
.body(patch.toUpdateOfferRequest)
.header(SessionHeaderName, session.toString)
.response(asJsonEither[OfsApiClientError, OfferResponse])
.readTimeout(ofsConfig.requestTimeout)
.send(sttpBackend)
.map(_.body.leftMap(parseFailure))
.recover(error => Left(OfsUnknownClientError(error.getMessage)))

EitherT(response)
}

def deleteOffer(session: Session, offerId: OfferID): EitherT[F, OfsClientError, OkResponse] = {
val requestUri = uri"${ofsConfig.baseUrl}/api/v1/offer/$offerId"

val response = basicRequest
.delete(requestUri)
.header(SessionHeaderName, session.toString)
.response(asJsonEither[OfsApiClientError, OkResponse])
.readTimeout(ofsConfig.requestTimeout)
.send(sttpBackend)
.map(_.body.leftMap(parseFailure))
.recover(error => Left(OfsUnknownClientError(error.getMessage)))

EitherT(response)
}
Expand Down
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.OfferID

case class GetOffersRequest(offerIds: List[OfferID])
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.github.mmvpm.bot.client.ofs.request

import com.github.mmvpm.model.Money

case class UpdateOfferRequest(
name: Option[String] = None,
price: Option[Money] = None,
description: Option[String] = None
)
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.Offer

case class OfferResponse(offer: Offer)
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.Offer

case class OffersResponse(offers: List[Offer])
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package com.github.mmvpm.bot.client.ofs.response

case class OkResponse()
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.github.mmvpm.bot.client.ofs.util

import com.github.mmvpm.model.OfferStatus.OfferStatus
import com.github.mmvpm.model.UserStatus.UserStatus
import com.github.mmvpm.model._
import io.circe._
import io.circe.generic.semiauto._

import java.util.UUID

case object CirceInstances {

// common

implicit val decoderUUID: Decoder[UUID] = Decoder[String].map(UUID.fromString)
implicit val encoderUUID: Encoder[UUID] = Encoder[String].contramap(_.toString)

// model

implicit val decoderUserStatus: Decoder[UserStatus] = Decoder.decodeEnumeration(UserStatus)
implicit val encoderUserStatus: Encoder[UserStatus] = Encoder.encodeEnumeration(UserStatus)

implicit val decoderUser: Decoder[User] = deriveDecoder
implicit val encoderUser: Encoder[User] = deriveEncoder

implicit val decoderOfferStatus: Decoder[OfferStatus] = Decoder.decodeEnumeration(OfferStatus)
implicit val encoderOfferStatus: Encoder[OfferStatus] = Encoder.encodeEnumeration(OfferStatus)

implicit val decoderOfferDescription: Decoder[OfferDescription] = deriveDecoder
implicit val encoderOfferDescription: Encoder[OfferDescription] = deriveEncoder

implicit val decoderOffer: Decoder[Offer] = deriveDecoder
implicit val encoderOffer: Encoder[Offer] = deriveEncoder
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,17 @@ 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
import com.github.mmvpm.bot.model.{Draft, OfferPatch}
import com.github.mmvpm.model.{Offer, OfferDescription, OfferID}

trait OfsManager[F[_]] {
def login(implicit message: Message): EitherT[F, OfsError, LoginOrRegisterResponse.LoggedIn]
def loginOrRegister(implicit message: Message): EitherT[F, OfsError, LoginOrRegisterResponse]

def getOffer(offerId: OfferID): EitherT[F, OfsError, Option[Offer]]
def getOffers(offerIds: Seq[OfferID]): EitherT[F, OfsError, List[Offer]]
def getMyOffers(implicit message: Message): EitherT[F, OfsError, List[Offer]]
def createOffer(description: OfferDescription)(implicit message: Message): EitherT[F, OfsError, Unit]
def updateOffer(offerId: OfferID, patch: OfferPatch)(implicit message: Message): EitherT[F, OfsError, Unit]
def deleteOffer(offerId: OfferID)(implicit message: Message): EitherT[F, OfsError, Unit]
}
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
package com.github.mmvpm.bot.manager.ofs

import cats.Monad
import cats.data.EitherT
import cats.effect.std.Random
import cats.implicits.{toFunctorOps, toTraverseOps}
import cats.{Functor, Monad}
import com.bot4s.telegram.models.Message
import com.github.mmvpm.bot.client.ofs.{OfsClient, error}
import com.github.mmvpm.bot.client.ofs.error.OfsApiClientError
import com.github.mmvpm.bot.client.ofs.response.{CreateOfferResponse, UserIdResponse}
import com.github.mmvpm.bot.client.ofs.error.{OfsApiClientError, OfsClientError}
import com.github.mmvpm.bot.client.ofs.response.UserIdResponse
import com.github.mmvpm.bot.manager.ofs.OfsManagerImpl._
import com.github.mmvpm.bot.manager.ofs.error.OfsError
import com.github.mmvpm.bot.manager.ofs.error.OfsError._
import com.github.mmvpm.bot.manager.ofs.response.LoginOrRegisterResponse
import com.github.mmvpm.bot.manager.ofs.response.LoginOrRegisterResponse._
import com.github.mmvpm.bot.model.{Draft, OfferPatch}
import com.github.mmvpm.bot.state.Storage
import com.github.mmvpm.model.{OfferDescription, Session}
import com.github.mmvpm.model.{Offer, OfferDescription, OfferID, OfferStatus, Session}
import sttp.model.StatusCode

class OfsManagerImpl[F[_]: Monad](ofsClient: OfsClient[F], sessionStorage: Storage[Option[Session]], random: Random[F])
Expand All @@ -32,13 +33,46 @@ class OfsManagerImpl[F[_]: Monad](ofsClient: OfsClient[F], sessionStorage: Stora
override def loginOrRegister(implicit message: Message): EitherT[F, OfsError, LoginOrRegisterResponse] =
sessionStorage.get match {
case None => registerAndSaveSession
case Some(session) => checkSession(session).map(_ => LoggedIn(getName))
case Some(session) => checkSession(session).as(LoggedIn(getName))
}

override def createOffer(description: OfferDescription)(implicit message: Message): EitherT[F, OfsError, Unit] =
def getOffer(offerId: OfferID): EitherT[F, OfsError, Option[Offer]] =
ofsClient
.getOffer(offerId)
.map(response => Option(response.offer))
.recover { case OfsApiClientError(_, StatusCode.NotFound.code, _) =>
None
}
.handleDefaultErrors

def getOffers(offerIds: Seq[OfferID]): EitherT[F, OfsError, List[Offer]] =
ofsClient
.getOffers(offerIds)
.map(_.offers)
.handleDefaultErrors

def getMyOffers(implicit message: Message): EitherT[F, OfsError, List[Offer]] =
sessionStorage.get match {
case None => EitherT.leftT(InvalidSession)
case Some(session) => getMyOffers(session)
}

def createOffer(description: OfferDescription)(implicit message: Message): EitherT[F, OfsError, Unit] =
sessionStorage.get match {
case None => EitherT.leftT(InvalidSession)
case Some(session) => createOffer(session, description).void
case Some(session) => createOffer(session, description)
}

def updateOffer(offerId: OfferID, patch: OfferPatch)(implicit message: Message): EitherT[F, OfsError, Unit] =
sessionStorage.get match {
case None => EitherT.leftT(InvalidSession)
case Some(session) => updateOffer(session, offerId, patch)
}

def deleteOffer(offerId: OfferID)(implicit message: Message): EitherT[F, OfsError, Unit] =
sessionStorage.get match {
case None => EitherT.leftT(InvalidSession)
case Some(session) => deleteOffer(session, offerId)
}

// internal
Expand All @@ -61,10 +95,7 @@ class OfsManagerImpl[F[_]: Monad](ofsClient: OfsClient[F], sessionStorage: Stora
private def checkSession(session: Session): EitherT[F, OfsError, UserIdResponse] =
ofsClient
.whoami(session)
.leftMap {
case OfsApiClientError(_, _, _) => InvalidSession: OfsError
case error => OfsSomeError(error.details)
}
.handleDefaultErrors

private def registerAndSaveSession(implicit message: Message): EitherT[F, OfsError, LoginOrRegisterResponse] =
(for {
Expand All @@ -78,15 +109,40 @@ class OfsManagerImpl[F[_]: Monad](ofsClient: OfsClient[F], sessionStorage: Stora
case error => OfsSomeError(error.details)
}

private def createOffer(session: Session, description: OfferDescription): EitherT[F, OfsError, CreateOfferResponse] =
private def getMyOffers(session: Session): EitherT[F, OfsError, List[Offer]] =
ofsClient
.getMyOffers(session)
.map(_.offers.filter(_.status == OfferStatus.Active))
.handleDefaultErrors

private def createOffer(session: Session, description: OfferDescription): EitherT[F, OfsError, Unit] =
ofsClient
.createOffer(session, description)
.leftMap {
case OfsApiClientError(_, StatusCode.Unauthorized.code, _) => InvalidSession: OfsError
case error => OfsSomeError(error.details)
}
.void
.handleDefaultErrors

private def deleteOffer(session: Session, offerId: OfferID): EitherT[F, OfsError, Unit] =
ofsClient
.deleteOffer(session, offerId)
.void
.handleDefaultErrors

private def updateOffer(session: Session, offerId: OfferID, patch: OfferPatch): EitherT[F, OfsError, Unit] =
ofsClient
.updateOffer(session, offerId, patch)
.void
.handleDefaultErrors
}

object OfsManagerImpl {

private val PasswordLength = 10

implicit class RichClientResponse[F[_]: Functor, R](response: EitherT[F, OfsClientError, R]) {
def handleDefaultErrors: EitherT[F, OfsError, R] =
response.leftMap {
case OfsApiClientError(_, StatusCode.Unauthorized.code, _) => InvalidSession: OfsError
case error => OfsSomeError(error.details)
}
}
}
Loading

0 comments on commit 5d1d26a

Please sign in to comment.