Use "conversation fsm" for book and chat actors

This commit is contained in:
Eugene Zadyra
2018-07-03 22:24:27 +02:00
parent fd8b1a7040
commit 207adfc96f
20 changed files with 233 additions and 354 deletions

View File

@@ -34,7 +34,9 @@ import com.lbs.server.service.DataService
class Account(val userId: UserId, bot: Bot, dataService: DataService, val localization: Localization, router: ActorRef) extends Conversation[Unit] with Localizable {
def askAction: QA =
entryPoint(askAction)
def askAction: Step =
question { _ =>
val credentials = dataService.getUserCredentials(userId.userId)
val currentAccount = credentials.find(c => c.accountId == userId.accountId).getOrElse(sys.error("Can't determine current account"))
@@ -68,8 +70,6 @@ class Account(val userId: UserId, bot: Bot, dataService: DataService, val locali
}
}
}
entryPoint(askAction)
}
object Account {

View File

@@ -26,8 +26,8 @@ package com.lbs.server.actor
import akka.actor.{Actor, ActorRef, PoisonPill, Props}
import com.lbs.bot.model.{Command, MessageSource}
import com.lbs.common.Logger
import com.lbs.server.actor.Chat.Init
import com.lbs.server.actor.Login.{LoggedIn, UserId}
import com.lbs.server.actor.conversation.Conversation.{InitConversation, StartConversation}
import com.lbs.server.service.DataService
import com.lbs.server.util.MessageExtractors._
@@ -47,7 +47,8 @@ class Auth(val source: MessageSource, dataService: DataService, unauthorizedHelp
unauthorizedHelpActor ! cmd
case cmd@Command(_, Text("/login"), _) =>
userId = None
loginActor ! Init
loginActor ! InitConversation
loginActor ! StartConversation
loginActor ! cmd
case cmd: Command if userId.isEmpty =>
loginActor ! cmd

View File

@@ -30,170 +30,153 @@ import com.lbs.api.json.model._
import com.lbs.bot._
import com.lbs.bot.model.{Button, Command}
import com.lbs.server.actor.Book._
import com.lbs.server.actor.Chat.Init
import com.lbs.server.actor.DatePicker.{DateFromMode, DateToMode}
import com.lbs.server.actor.Login.UserId
import com.lbs.server.actor.StaticData.StaticDataConfig
import com.lbs.server.actor.conversation.Conversation
import com.lbs.server.actor.conversation.Conversation.{InitConversation, StartConversation}
import com.lbs.server.lang.{Localizable, Localization}
import com.lbs.server.repository.model.Monitoring
import com.lbs.server.service.{ApiService, DataService, MonitoringService}
import com.lbs.server.util.ServerModelConverters._
import scala.util.{Failure, Success, Try}
class Book(val userId: UserId, bot: Bot, apiService: ApiService, dataService: DataService, monitoringService: MonitoringService,
val localization: Localization, datePickerActorFactory: ByUserIdWithOriginatorActorFactory, staticDataActorFactory: ByUserIdWithOriginatorActorFactory,
termsPagerActorFactory: ByUserIdWithOriginatorActorFactory) extends SafeFSM[FSMState, FSMData] with StaticDataForBooking with Localizable {
termsPagerActorFactory: ByUserIdWithOriginatorActorFactory) extends Conversation[BookingData] with StaticDataForBooking with Localizable {
private val datePicker = datePickerActorFactory(userId, self)
protected val staticData = staticDataActorFactory(userId, self)
private[actor] val staticData = staticDataActorFactory(userId, self)
private val termsPager = termsPagerActorFactory(userId, self)
startWith(RequestCity, BookingData())
entryPoint(askCity, BookingData())
requestStaticData(RequestCity, AwaitCity, cityConfig) { bd: BookingData =>
withFunctions(
latestOptions = dataService.getLatestCities(userId.accountId),
staticOptions = apiService.getAllCities(userId.accountId),
applyId = id => bd.copy(cityId = id))
}(requestNext = RequestClinic)
private def askCity: Step =
staticData(cityConfig) { bd: BookingData =>
withFunctions(
latestOptions = dataService.getLatestCities(userId.accountId),
staticOptions = apiService.getAllCities(userId.accountId),
applyId = id => bd.copy(cityId = id))
}(requestNext = askClinic)
requestStaticData(RequestClinic, AwaitClinic, clinicConfig) { bd: BookingData =>
withFunctions(
latestOptions = dataService.getLatestClinicsByCityId(userId.accountId, bd.cityId.id),
staticOptions = apiService.getAllClinics(userId.accountId, bd.cityId.id),
applyId = id => bd.copy(clinicId = id))
}(requestNext = RequestService)
private def askClinic: Step =
staticData(clinicConfig) { bd: BookingData =>
withFunctions(
latestOptions = dataService.getLatestClinicsByCityId(userId.accountId, bd.cityId.id),
staticOptions = apiService.getAllClinics(userId.accountId, bd.cityId.id),
applyId = id => bd.copy(clinicId = id))
}(requestNext = askService)
requestStaticData(RequestService, AwaitService, serviceConfig) { bd: BookingData =>
withFunctions(
latestOptions = dataService.getLatestServicesByCityIdAndClinicId(userId.accountId, bd.cityId.id, bd.clinicId.optionalId),
staticOptions = apiService.getAllServices(userId.accountId, bd.cityId.id, bd.clinicId.optionalId),
applyId = id => bd.copy(serviceId = id))
}(requestNext = RequestDoctor)
private def askService: Step =
staticData(serviceConfig) { bd: BookingData =>
withFunctions(
latestOptions = dataService.getLatestServicesByCityIdAndClinicId(userId.accountId, bd.cityId.id, bd.clinicId.optionalId),
staticOptions = apiService.getAllServices(userId.accountId, bd.cityId.id, bd.clinicId.optionalId),
applyId = id => bd.copy(serviceId = id))
}(requestNext = askDoctor)
requestStaticData(RequestDoctor, AwaitDoctor, doctorConfig) { bd: BookingData =>
withFunctions(
latestOptions = dataService.getLatestDoctorsByCityIdAndClinicIdAndServiceId(userId.accountId, bd.cityId.id, bd.clinicId.optionalId, bd.serviceId.id),
staticOptions = apiService.getAllDoctors(userId.accountId, bd.cityId.id, bd.clinicId.optionalId, bd.serviceId.id),
applyId = id => bd.copy(doctorId = id))
}(requestNext = RequestDateFrom)
private def askDoctor: Step =
staticData(doctorConfig) { bd: BookingData =>
withFunctions(
latestOptions = dataService.getLatestDoctorsByCityIdAndClinicIdAndServiceId(userId.accountId, bd.cityId.id, bd.clinicId.optionalId, bd.serviceId.id),
staticOptions = apiService.getAllDoctors(userId.accountId, bd.cityId.id, bd.clinicId.optionalId, bd.serviceId.id),
applyId = id => bd.copy(doctorId = id))
}(requestNext = requestDateFrom)
whenSafe(RequestDateFrom) {
case Event(_, bookingData: BookingData) =>
private def requestDateFrom: Step =
question { bookingData =>
datePicker ! InitConversation
datePicker ! StartConversation
datePicker ! DateFromMode
datePicker ! bookingData.dateFrom
goto(AwaitDateFrom)
}
} answer {
case Msg(cmd: Command, _) =>
datePicker ! cmd
stay()
case Msg(date: ZonedDateTime, bookingData: BookingData) =>
goto(requestDateTo) using bookingData.copy(dateFrom = date)
}
whenSafe(AwaitDateFrom) {
case Event(cmd: Command, _) =>
datePicker ! cmd
stay()
case Event(date: ZonedDateTime, bookingData: BookingData) =>
invokeNext()
goto(RequestDateTo) using bookingData.copy(dateFrom = date)
}
whenSafe(RequestDateTo) {
case Event(_, bookingData: BookingData) =>
private def requestDateTo: Step =
question { bookingData =>
datePicker ! InitConversation
datePicker ! StartConversation
datePicker ! DateToMode
datePicker ! bookingData.dateFrom.plusDays(1)
goto(AwaitDateTo)
}
} answer {
case Msg(cmd: Command, _) =>
datePicker ! cmd
stay()
case Msg(date: ZonedDateTime, bookingData: BookingData) =>
goto(requestDayTime) using bookingData.copy(dateTo = date)
}
whenSafe(AwaitDateTo) {
case Event(cmd: Command, _) =>
datePicker ! cmd
stay()
case Event(date: ZonedDateTime, bookingData: BookingData) =>
invokeNext()
goto(RequestDayTime) using bookingData.copy(dateTo = date)
}
whenSafe(RequestDayTime) {
case Event(Next, _: BookingData) =>
private def requestDayTime: Step =
question { _ =>
bot.sendMessage(userId.source, lang.chooseTimeOfDay,
inlineKeyboard = createInlineKeyboard(lang.timeOfDay.map { case (id, label) => Button(label, id.toString) }.toSeq, columns = 1))
goto(AwaitDayTime)
}
} answer {
case Msg(Command(_, msg, Some(timeIdStr)), bookingData: BookingData) =>
val timeId = timeIdStr.toInt
bot.sendEditMessage(userId.source, msg.messageId, lang.preferredTimeIs(timeId))
goto(requestAction) using bookingData.copy(timeOfDay = timeId)
}
whenSafe(AwaitDayTime) {
case Event(Command(_, msg, Some(timeIdStr)), bookingData: BookingData) =>
invokeNext()
val timeId = timeIdStr.toInt
bot.sendEditMessage(userId.source, msg.messageId, lang.preferredTimeIs(timeId))
goto(RequestAction) using bookingData.copy(timeOfDay = timeId)
}
whenSafe(RequestAction) {
case Event(Next, bookingData: BookingData) =>
private def requestAction: Step =
question { bookingData =>
dataService.storeAppointment(userId.accountId, bookingData)
bot.sendMessage(userId.source,
lang.bookingSummary(bookingData),
inlineKeyboard = createInlineKeyboard(Seq(Button(lang.findTerms, Tags.FindTerms), Button(lang.modifyDate, Tags.ModifyDate))))
goto(AwaitAction)
}
} answer {
case Msg(Command(_, _, Some(Tags.FindTerms)), _) =>
goto(requestTerm)
case Msg(Command(_, _, Some(Tags.ModifyDate)), _) =>
goto(requestDateFrom)
}
whenSafe(AwaitAction) {
case Event(Command(_, _, Some(Tags.FindTerms)), _) =>
invokeNext()
goto(RequestTerm)
case Event(Command(_, _, Some(Tags.ModifyDate)), _) =>
invokeNext()
goto(RequestDateFrom)
}
whenSafe(RequestTerm) {
case Event(Next, bookingData: BookingData) =>
private def requestTerm: Step =
question { bookingData =>
val availableTerms = apiService.getAvailableTerms(userId.accountId, bookingData.cityId.id,
bookingData.clinicId.optionalId, bookingData.serviceId.id, bookingData.doctorId.optionalId,
bookingData.dateFrom, Some(bookingData.dateTo), timeOfDay = bookingData.timeOfDay)
termsPager ! InitConversation
termsPager ! StartConversation
termsPager ! availableTerms
goto(AwaitTerm)
}
} answer {
case Msg(cmd: Command, _) =>
termsPager ! cmd
stay()
case Msg(term: AvailableVisitsTermPresentation, bookingData) =>
val response = apiService.temporaryReservation(userId.accountId, term.mapTo[TemporaryReservationRequest], term.mapTo[ValuationsRequest])
response match {
case Left(ex) =>
bot.sendMessage(userId.source, ex.getMessage)
end()
case Right((temporaryReservation, valuations)) =>
bot.sendMessage(userId.source, lang.confirmAppointment(term, valuations),
inlineKeyboard = createInlineKeyboard(Seq(Button(lang.cancel, Tags.Cancel), Button(lang.book, Tags.Book))))
goto(awaitReservation) using bookingData.copy(term = Some(term), temporaryReservationId = Some(temporaryReservation.id), valuations = Some(valuations))
}
case Msg(Pager.NoItemsFound, _) =>
goto(askNoTermsAction)
}
whenSafe(AwaitTerm) {
case Event(Command(_, _, Some(Tags.ModifyDate)), _) =>
invokeNext()
goto(RequestDateFrom)
case Event(Command(_, _, Some(Tags.CreateMonitoring)), _) =>
invokeNext()
goto(AskMonitoringOptions)
case Event(cmd: Command, _) =>
termsPager ! cmd
stay()
case Event(term: AvailableVisitsTermPresentation, _) =>
self ! term
goto(RequestReservation)
case Event(Pager.NoItemsFound, _) =>
private def askNoTermsAction: Step =
question { _ =>
bot.sendMessage(userId.source, lang.noTermsFound, inlineKeyboard =
createInlineKeyboard(Seq(Button(lang.modifyDate, Tags.ModifyDate), Button(lang.createMonitoring, Tags.CreateMonitoring))))
stay()
}
} answer {
case Msg(Command(_, _, Some(Tags.ModifyDate)), _) =>
goto(requestDateFrom)
case Msg(Command(_, _, Some(Tags.CreateMonitoring)), _) =>
goto(askMonitoringOptions)
}
whenSafe(RequestReservation) {
case Event(term: AvailableVisitsTermPresentation, bookingData: BookingData) =>
val response = apiService.temporaryReservation(userId.accountId, term.mapTo[TemporaryReservationRequest], term.mapTo[ValuationsRequest])
response match {
case Left(ex) =>
bot.sendMessage(userId.source, ex.getMessage)
invokeNext()
stay()
case Right((temporaryReservation, valuations)) =>
bot.sendMessage(userId.source, lang.confirmAppointment(term, valuations),
inlineKeyboard = createInlineKeyboard(Seq(Button(lang.cancel, Tags.Cancel), Button(lang.book, Tags.Book))))
goto(AwaitReservation) using bookingData.copy(term = Some(term), temporaryReservationId = Some(temporaryReservation.id), valuations = Some(valuations))
}
}
whenSafe(AwaitReservation) {
case Event(Command(_, _, Some(Tags.Cancel)), bookingData: BookingData) =>
private def awaitReservation: Step = monologue {
case Msg(Command(_, _, Some(Tags.Cancel)), bookingData: BookingData) =>
apiService.deleteTemporaryReservation(userId.accountId, bookingData.temporaryReservationId.get)
stay()
case Event(Command(_, _, Some(Tags.Book)), bookingData: BookingData) =>
case Msg(Command(_, _, Some(Tags.Book)), bookingData: BookingData) =>
val reservationRequestMaybe = for {
tmpReservationId <- bookingData.temporaryReservationId
valuations <- bookingData.valuations
@@ -205,49 +188,41 @@ class Book(val userId: UserId, bot: Bot, apiService: ApiService, dataService: Da
case Some(reservationRequest) =>
apiService.reservation(userId.accountId, reservationRequest) match {
case Left(ex) =>
error("Error during reservation", ex)
bot.sendMessage(userId.source, ex.getMessage)
invokeNext()
stay()
end()
case Right(success) =>
log.debug(s"Successfully confirmed: $success")
debug(s"Successfully confirmed: $success")
bot.sendMessage(userId.source, lang.appointmentIsConfirmed)
stay()
end()
}
case _ => sys.error(s"Can not prepare reservation request using booking data $bookingData")
}
}
whenSafe(AskMonitoringOptions) {
case Event(Next, _) =>
private def askMonitoringOptions: Step =
question { _ =>
bot.sendMessage(userId.source, lang.chooseTypeOfMonitoring,
inlineKeyboard = createInlineKeyboard(Seq(Button(lang.bookByApplication, Tags.BookByApplication), Button(lang.bookManually, Tags.BookManually)), columns = 1))
stay()
case Event(Command(_, _, Some(autobookStr)), bookingData: BookingData) =>
val autobook = autobookStr.toBoolean
invokeNext()
goto(CreateMonitoring) using bookingData.copy(autobook = autobook)
}
} answer {
case Msg(Command(_, _, Some(autobookStr)), bookingData: BookingData) =>
val autobook = autobookStr.toBoolean
goto(createMonitoring) using bookingData.copy(autobook = autobook)
}
whenSafe(CreateMonitoring) {
case Event(Next, bookingData: BookingData) =>
private def createMonitoring: Step =
internalConfig { bookingData =>
debug(s"Creating monitoring for $bookingData")
Try(monitoringService.createMonitoring((userId -> bookingData).mapTo[Monitoring])) match {
case Success(_) => bot.sendMessage(userId.source, lang.monitoringHasBeenCreated)
case Failure(ex) =>
try {
monitoringService.createMonitoring((userId -> bookingData).mapTo[Monitoring])
bot.sendMessage(userId.source, lang.monitoringHasBeenCreated)
} catch {
case ex: Exception =>
error("Unable to create monitoring", ex)
bot.sendMessage(userId.source, lang.unableToCreateMonitoring)
}
goto(RequestCity) using BookingData()
}
whenUnhandledSafe {
case Event(Init, _) =>
reinit()
case e: Event =>
error(s"Unhandled event in state:$stateName. Event: $e")
stay()
}
end()
}
private def cityConfig = StaticDataConfig(lang.city, "Wrocław", isAnyAllowed = false)
@@ -257,16 +232,6 @@ class Book(val userId: UserId, bot: Bot, apiService: ApiService, dataService: Da
private def doctorConfig = StaticDataConfig(lang.doctor, "Bartniak", isAnyAllowed = true)
private def reinit() = {
invokeNext()
datePicker ! InitConversation
staticData ! InitConversation
termsPager ! Init
goto(RequestCity) using BookingData()
}
initialize()
override def postStop(): Unit = {
datePicker ! PoisonPill
staticData ! PoisonPill
@@ -283,54 +248,10 @@ object Book {
Props(new Book(userId, bot, apiService, dataService, monitoringService, localization, datePickerActorFactory,
staticDataActorFactory, termsPagerActorFactory))
object RequestCity extends FSMState
object AwaitCity extends FSMState
object RequestClinic extends FSMState
object AwaitClinic extends FSMState
object RequestService extends FSMState
object AwaitService extends FSMState
object RequestDoctor extends FSMState
object AwaitDoctor extends FSMState
object CreateMonitoring extends FSMState
object AskMonitoringOptions extends FSMState
object RequestDateFrom extends FSMState
object AwaitDateFrom extends FSMState
object RequestDateTo extends FSMState
object AwaitDateTo extends FSMState
object RequestDayTime extends FSMState
object AwaitDayTime extends FSMState
object RequestAction extends FSMState
object AwaitAction extends FSMState
object RequestTerm extends FSMState
object AwaitTerm extends FSMState
object RequestReservation extends FSMState
object AwaitReservation extends FSMState
case class BookingData(cityId: IdName = null, clinicId: IdName = null,
serviceId: IdName = null, doctorId: IdName = null, dateFrom: ZonedDateTime = ZonedDateTime.now(),
dateTo: ZonedDateTime = ZonedDateTime.now().plusDays(1L), timeOfDay: Int = 0, autobook: Boolean = false, term: Option[AvailableVisitsTermPresentation] = None,
temporaryReservationId: Option[Long] = None, valuations: Option[ValuationsResponse] = None) extends FSMData
temporaryReservationId: Option[Long] = None, valuations: Option[ValuationsResponse] = None)
object Tags {
val Cancel = "cancel"

View File

@@ -51,7 +51,7 @@ class Bug(val userId: UserId, bot: Bot, dataService: DataService, bugPagerActorF
goto(displaySubmittedBugs)
}
def displaySubmittedBugs: IC =
def displaySubmittedBugs: Step =
internalConfig { _ =>
val bugs = dataService.getBugs(userId.userId)
bugPager ! InitConversation
@@ -60,7 +60,7 @@ class Bug(val userId: UserId, bot: Bot, dataService: DataService, bugPagerActorF
goto(processResponseFromPager)
}
def processResponseFromPager: M =
def processResponseFromPager: Step =
monologue {
case Msg(cmd: Command, _) =>
bugPager ! cmd

View File

@@ -28,6 +28,7 @@ import com.lbs.bot.model.Command
import com.lbs.common.Logger
import com.lbs.server.actor.Chat._
import com.lbs.server.actor.Login.UserId
import com.lbs.server.actor.conversation.Conversation
import com.lbs.server.actor.conversation.Conversation.{InitConversation, StartConversation}
import com.lbs.server.service.{DataService, MonitoringService}
import com.lbs.server.util.MessageExtractors._
@@ -37,7 +38,7 @@ import scala.util.matching.Regex
class Chat(val userId: UserId, dataService: DataService, monitoringService: MonitoringService, bookingActorFactory: ByUserIdActorFactory, helpActorFactory: ByUserIdActorFactory,
monitoringsActorFactory: ByUserIdActorFactory, historyActorFactory: ByUserIdActorFactory,
visitsActorFactory: ByUserIdActorFactory, settingsActorFactory: ByUserIdActorFactory,
bugActorFactory: ByUserIdActorFactory, accountActorFactory: ByUserIdActorFactory) extends SafeFSM[FSMState, FSMData] with Logger {
bugActorFactory: ByUserIdActorFactory, accountActorFactory: ByUserIdActorFactory) extends Conversation[Unit] with Logger {
private val bookingActor = bookingActorFactory(userId)
private val helpActor = helpActorFactory(userId)
@@ -48,124 +49,115 @@ class Chat(val userId: UserId, dataService: DataService, monitoringService: Moni
private val bugActor = bugActorFactory(userId)
private val accountActor = accountActorFactory(userId)
startWith(HelpChat, null)
entryPoint(helpChat)
when(HelpChat, helpActor) {
case Event(cmd@Command(_, Text("/help"), _), _) =>
private def helpChat: Step = actorDialogue(helpActor) {
case Msg(cmd@Command(_, Text("/help"), _), _) =>
helpActor ! cmd
stay()
case Event(cmd@Command(_, Text("/start"), _), _) =>
case Msg(cmd@Command(_, Text("/start"), _), _) =>
helpActor ! cmd
stay()
}
when(BookChat, bookingActor) {
case Event(Command(_, Text("/book"), _), _) =>
bookingActor ! Init
private def bookChat: Step = actorDialogue(bookingActor) {
case Msg(Command(_, Text("/book"), _), _) =>
bookingActor ! InitConversation
bookingActor ! StartConversation
stay()
}
when(HistoryChat, historyActor) {
case Event(Command(_, Text("/history"), _), _) =>
private def historyChat: Step = actorDialogue(historyActor) {
case Msg(Command(_, Text("/history"), _), _) =>
historyActor ! InitConversation
historyActor ! StartConversation
stay()
}
when(VisitsChat, visitsActor) {
case Event(Command(_, Text("/reserved"), _), _) =>
private def visitsChat: Step = actorDialogue(visitsActor) {
case Msg(Command(_, Text("/reserved"), _), _) =>
visitsActor ! InitConversation
visitsActor ! StartConversation
stay()
}
when(BugChat, bugActor) {
case Event(Command(_, Text("/bug"), _), _) =>
private def bugChat: Step = actorDialogue(bugActor) {
case Msg(Command(_, Text("/bug"), _), _) =>
bugActor ! InitConversation
bugActor ! StartConversation
goto(BugChat)
stay()
}
when(MonitoringsChat, monitoringsActor) {
case Event(Command(_, Text("/monitorings"), _), _) =>
private def monitoringsChat: Step = actorDialogue(monitoringsActor) {
case Msg(Command(_, Text("/monitorings"), _), _) =>
monitoringsActor ! InitConversation
monitoringsActor ! StartConversation
stay()
}
when(SettingsChat, settingsActor) {
case Event(Command(_, Text("/settings"), _), _) =>
private def settingsChat: Step = actorDialogue(settingsActor) {
case Msg(Command(_, Text("/settings"), _), _) =>
settingsActor ! InitConversation
settingsActor ! StartConversation
stay()
}
when(AccountChat, accountActor) {
case Event(Command(_, Text("/accounts"), _), _) =>
private def accountChat: Step = actorDialogue(accountActor) {
case Msg(Command(_, Text("/accounts"), _), _) =>
accountActor ! InitConversation
accountActor ! StartConversation
stay()
}
private def when(state: FSMState, actor: ActorRef)(mainStateFunction: StateFunction): Unit = {
whenSafe(state) {
case event: Event =>
private def actorDialogue(actor: ActorRef)(mainStateFunction: AnswerFn): Step =
monologue {
case event: Msg =>
if (mainStateFunction.isDefinedAt(event)) mainStateFunction(event)
else {
val secondaryStateFunction = secondaryState(actor)
if (secondaryStateFunction.isDefinedAt(event)) secondaryStateFunction(event)
else eventHandler(event)
secondaryStateFunction(event)
}
}
}
private def secondaryState(actor: ActorRef): StateFunction = {
case Event(cmd@Command(_, Text("/bug"), _), _) =>
private def secondaryState(actor: ActorRef): AnswerFn = {
case Msg(cmd@Command(_, Text("/bug"), _), _) =>
self ! cmd
goto(BugChat)
case Event(cmd@Command(_, Text("/help"), _), _) =>
goto(bugChat)
case Msg(cmd@Command(_, Text("/help"), _), _) =>
self ! cmd
goto(HelpChat)
case Event(cmd@Command(_, Text("/start"), _), _) =>
goto(helpChat)
case Msg(cmd@Command(_, Text("/start"), _), _) =>
self ! cmd
goto(HelpChat)
case Event(cmd@Command(_, Text("/book"), _), _) =>
goto(helpChat)
case Msg(cmd@Command(_, Text("/book"), _), _) =>
self ! cmd
goto(BookChat)
case Event(cmd@Command(_, Text("/monitorings"), _), _) =>
goto(bookChat)
case Msg(cmd@Command(_, Text("/monitorings"), _), _) =>
self ! cmd
goto(MonitoringsChat)
case Event(cmd@Command(_, Text("/history"), _), _) =>
goto(monitoringsChat)
case Msg(cmd@Command(_, Text("/history"), _), _) =>
self ! cmd
goto(HistoryChat)
case Event(cmd@Command(_, Text("/reserved"), _), _) =>
goto(historyChat)
case Msg(cmd@Command(_, Text("/reserved"), _), _) =>
self ! cmd
goto(VisitsChat)
case Event(cmd@Command(_, Text("/settings"), _), _) =>
goto(visitsChat)
case Msg(cmd@Command(_, Text("/settings"), _), _) =>
self ! cmd
goto(SettingsChat)
case Event(cmd@Command(_, Text("/accounts"), _), _) =>
goto(settingsChat)
case Msg(cmd@Command(_, Text("/accounts"), _), _) =>
self ! cmd
goto(AccountChat)
case Event(cmd@Command(_, Text(MonitoringId(monitoringIdStr, scheduleIdStr, timeStr)), _), _) =>
goto(accountChat)
case Msg(cmd@Command(_, Text(MonitoringId(monitoringIdStr, scheduleIdStr, timeStr)), _), _) =>
val monitoringId = monitoringIdStr.toLong
val scheduleId = scheduleIdStr.toLong
val time = timeStr.toLong
monitoringService.bookAppointmentByScheduleId(userId.accountId, monitoringId, scheduleId, time)
stay()
case Event(cmd: Command, _) =>
case Msg(cmd: Command, _) =>
actor ! cmd
stay()
}
whenUnhandledSafe {
case e: Event =>
debug(s"Unhandled event in state:$stateName. Event: $e")
stay()
}
initialize()
override def postStop(): Unit = {
bookingActor ! PoisonPill
helpActor ! PoisonPill
@@ -187,24 +179,6 @@ object Chat {
Props(new Chat(userId, dataService, monitoringService, bookingActorFactory, helpActorFactory, monitoringsActorFactory,
historyActorFactory, visitsActorFactory, settingsActorFactory, bugActorFactory, accountActorFactory))
object HelpChat extends FSMState
object BookChat extends FSMState
object MonitoringsChat extends FSMState
object HistoryChat extends FSMState
object VisitsChat extends FSMState
object SettingsChat extends FSMState
object BugChat extends FSMState
object AccountChat extends FSMState
object Init
val MonitoringId: Regex = s"/reserve_(\\d+)_(\\d+)_(\\d+)".r
}

View File

@@ -48,7 +48,7 @@ class DatePicker(val userId: UserId, val bot: Bot, val localization: Localizatio
entryPoint(configure)
def configure: EC =
def configure: Step =
externalConfig {
case Msg(newMode: Mode, _) =>
mode = newMode
@@ -57,7 +57,7 @@ class DatePicker(val userId: UserId, val bot: Bot, val localization: Localizatio
goto(requestDate) using initialDate
}
def requestDate: QA =
def requestDate: Step =
question { initialDate =>
val message = mode match {
case DateFromMode => lang.chooseDateFrom

View File

@@ -34,7 +34,7 @@ class Help(val userId: UserId, bot: Bot, val localization: Localization) extends
entryPoint(displayHelp)
def displayHelp: M =
def displayHelp: Step =
monologue {
case Msg(_: Command, _) =>
bot.sendMessage(userId.source, lang.help)

View File

@@ -39,7 +39,7 @@ class History(val userId: UserId, bot: Bot, apiService: ApiService, val localiza
entryPoint(prepareData)
def prepareData: IC =
def prepareData: Step =
internalConfig { _ =>
val visits = apiService.visitsHistory(userId.accountId)
historyPager ! InitConversation
@@ -48,7 +48,7 @@ class History(val userId: UserId, bot: Bot, apiService: ApiService, val localiza
goto(processResponseFromPager)
}
def processResponseFromPager: M =
def processResponseFromPager: Step =
monologue {
case Msg(cmd: Command, _) =>
historyPager ! cmd

View File

@@ -41,14 +41,14 @@ class Login(source: MessageSource, bot: Bot, dataService: DataService, apiServic
private var forwardCommand: ForwardCommand = _
def logIn: M =
def logIn: Step =
monologue {
case Msg(cmd: Command, LoginData(None, None)) =>
forwardCommand = ForwardCommand(cmd)
goto(requestUsername)
}
def requestUsername: QA =
def requestUsername: Step =
question { _ =>
bot.sendMessage(source, lang.provideUsername)
} answer {
@@ -56,7 +56,7 @@ class Login(source: MessageSource, bot: Bot, dataService: DataService, apiServic
goto(requestPassword) using LoginData(username = username)
}
def requestPassword: QA =
def requestPassword: Step =
question { _ =>
bot.sendMessage(source, lang.providePassword)
} answer {
@@ -64,7 +64,7 @@ class Login(source: MessageSource, bot: Bot, dataService: DataService, apiServic
goto(processLoginInformation) using loginData.copy(password = password.map(textEncryptor.encrypt))
}
def processLoginInformation: IC = {
def processLoginInformation: Step = {
internalConfig { case LoginData(Some(username), Some(password)) =>
val loginResult = apiService.login(username, password)
loginResult match {

View File

@@ -40,7 +40,7 @@ class Monitorings(val userId: UserId, bot: Bot, monitoringService: MonitoringSer
entryPoint(prepareData)
def prepareData: IC =
def prepareData: Step =
internalConfig { _ =>
val monitorings = monitoringService.getActiveMonitorings(userId.accountId)
monitoringsPager ! InitConversation
@@ -49,7 +49,7 @@ class Monitorings(val userId: UserId, bot: Bot, monitoringService: MonitoringSer
goto(processResponseFromPager)
}
def processResponseFromPager: M =
def processResponseFromPager: Step =
monologue {
case Msg(cmd: Command, _) =>
monitoringsPager ! cmd
@@ -61,7 +61,7 @@ class Monitorings(val userId: UserId, bot: Bot, monitoringService: MonitoringSer
goto(askToDeactivateMonitoring) using monitoring
}
def askToDeactivateMonitoring: QA =
def askToDeactivateMonitoring: Step =
question { monitoring =>
bot.sendMessage(userId.source, lang.deactivateMonitoring(monitoring), inlineKeyboard =
createInlineKeyboard(Seq(Button(lang.no, Tags.No), Button(lang.yes, Tags.Yes))))

View File

@@ -42,7 +42,7 @@ class Pager[Data](val userId: UserId, bot: Bot, makeMessage: (Data, Int, Int) =>
entryPoint(awaitForData)
private def awaitForData: EC =
private def awaitForData: Step =
externalConfig {
case Msg(Left(error: Throwable), _) =>
bot.sendMessage(userId.source, error.getMessage)
@@ -54,7 +54,7 @@ class Pager[Data](val userId: UserId, bot: Bot, makeMessage: (Data, Int, Int) =>
goto(displayPage) using Registry(0, items.grouped(Pager.PageSize).toList) -> None
}
private def displayPage: QA =
private def displayPage: Step =
question { case (registry, massageIdMaybe) =>
sendPage(registry.page, registry.pages, massageIdMaybe)
} answer {

View File

@@ -36,7 +36,7 @@ class Settings(val userId: UserId, bot: Bot, dataService: DataService, val local
entryPoint(askForAction)
def askForAction: QA =
def askForAction: Step =
question { _ =>
bot.sendMessage(userId.source, lang.settingsHeader, inlineKeyboard =
createInlineKeyboard(Seq(Button(lang.language, Tags.Language))))
@@ -45,7 +45,7 @@ class Settings(val userId: UserId, bot: Bot, dataService: DataService, val local
goto(askLanguage)
}
def askLanguage: QA =
def askLanguage: Step =
question { _ =>
bot.sendMessage(userId.source, lang.chooseLanguage,
inlineKeyboard = createInlineKeyboard(Lang.Langs.map(l => Button(l.label, l.id)), columns = 1))

View File

@@ -40,14 +40,14 @@ class StaticData(val userId: UserId, bot: Bot, val localization: Localization, o
entryPoint(AwaitConfig)
def AwaitConfig: EC =
def AwaitConfig: Step =
externalConfig {
case Msg(newConfig: StaticDataConfig, _) =>
config = newConfig
goto(askForLatestOption)
}
def askForLatestOption: QA =
def askForLatestOption: Step =
question { _ =>
originator ! LatestOptions
} answer {
@@ -59,7 +59,7 @@ class StaticData(val userId: UserId, bot: Bot, val localization: Localization, o
goto(askForUserInput) using callbackTags
}
def askForUserInput: QA =
def askForUserInput: Step =
question { callbackTags =>
bot.sendMessage(userId.source, lang.pleaseEnterStaticDataNameOrPrevious(config),
inlineKeyboard = createInlineKeyboard(callbackTags, columns = 1))

View File

@@ -28,41 +28,38 @@ import com.lbs.api.json.model.IdName
import com.lbs.bot.model.Command
import com.lbs.server.actor.Book.BookingData
import com.lbs.server.actor.StaticData.{FindOptions, FoundOptions, LatestOptions, StaticDataConfig}
import com.lbs.server.actor.conversation.Conversation
import com.lbs.server.actor.conversation.Conversation.{InitConversation, StartConversation}
trait StaticDataForBooking extends SafeFSM[FSMState, FSMData] {
trait StaticDataForBooking extends Conversation[BookingData] {
protected def staticData: ActorRef
private[actor] def staticData: ActorRef
protected def withFunctions(latestOptions: => Seq[IdName], staticOptions: => Either[Throwable, List[IdName]], applyId: IdName => BookingData): FSMState => StateFunction = {
nextState: FSMState => {
case Event(cmd: Command, _) =>
protected def withFunctions(latestOptions: => Seq[IdName], staticOptions: => Either[Throwable, List[IdName]], applyId: IdName => BookingData): Step => AnswerFn = {
nextStep: Step => {
case Msg(cmd: Command, _) =>
staticData ! cmd
stay()
case Event(LatestOptions, _) =>
case Msg(LatestOptions, _) =>
staticData ! LatestOptions(latestOptions)
stay()
case Event(FindOptions(searchText), _) =>
case Msg(FindOptions(searchText), _) =>
staticData ! FoundOptions(filterOptions(staticOptions, searchText))
stay()
case Event(id: IdName, _) =>
invokeNext()
goto(nextState) using applyId(id)
case Msg(id: IdName, _) =>
goto(nextStep) using applyId(id)
}
}
protected def requestStaticData(requestState: FSMState, awaitState: FSMState, staticDataConfig: => StaticDataConfig)(functions: BookingData => FSMState => StateFunction)(requestNext: FSMState): Unit = {
whenSafe(requestState) {
case Event(_, _) =>
staticData ! InitConversation
staticData ! StartConversation
staticData ! staticDataConfig
goto(awaitState)
}
whenSafe(awaitState) {
case event@Event(_, bookingData: BookingData) =>
protected def staticData(staticDataConfig: => StaticDataConfig)(functions: BookingData => Step => AnswerFn)(requestNext: Step): Step = {
question { _ =>
staticData ! InitConversation
staticData ! StartConversation
staticData ! staticDataConfig
} answer {
case msg@Msg(_, bookingData: BookingData) =>
val fn = functions(bookingData)(requestNext)
if (fn.isDefinedAt(event)) fn(event) else eventHandler(event)
fn(msg)
}
}

View File

@@ -41,7 +41,7 @@ class Visits(val userId: UserId, bot: Bot, apiService: ApiService, val localizat
entryPoint(prepareData)
def prepareData: IC =
def prepareData: Step =
internalConfig { _ =>
val visits = apiService.reservedVisits(userId.accountId)
reservedVisitsPager ! InitConversation
@@ -50,7 +50,7 @@ class Visits(val userId: UserId, bot: Bot, apiService: ApiService, val localizat
goto(processResponseFromPager)
}
def processResponseFromPager: M =
def processResponseFromPager: Step =
monologue {
case Msg(cmd: Command, _) =>
reservedVisitsPager ! cmd
@@ -62,7 +62,7 @@ class Visits(val userId: UserId, bot: Bot, apiService: ApiService, val localizat
goto(askToCancelVisit) using visit
}
def askToCancelVisit: QA =
def askToCancelVisit: Step =
question { visit =>
bot.sendMessage(userId.source, lang.areYouSureToCancelAppointment(visit),
inlineKeyboard = createInlineKeyboard(Seq(Button(lang.no, Tags.No), Button(lang.yes, Tags.Yes))))

View File

@@ -29,14 +29,6 @@ trait Domain[D] {
private[conversation] case class Answer(answerFn: AnswerFn)
protected type QA = QuestionAnswer
protected type EC = ExternalConfiguration
protected type IC = InternalConfiguration
protected type M = Monologue
protected implicit class RichQuestion(question: Question) {
def answer(answerFn: AnswerFn): QuestionAnswer = QuestionAnswer(question, Answer(answerFn))
}

View File

@@ -32,11 +32,4 @@ package object actor {
type ByUserIdActorFactory = UserId => ActorRef
type ByMessageSourceActorFactory = MessageSource => ActorRef
type ByMessageSourceWithOriginatorActorFactory = (MessageSource, ActorRef) => ActorRef
def invokeNext()(implicit self: ActorRef): Unit = {
self ! Next
}
object Next
}

View File

@@ -196,9 +196,9 @@ class MonitoringService extends Logger {
debug(s"Deactivating monitoring [#$monitoringId]")
if (!future.isCancelled) {
future.cancel(true)
monitoring.active = false
dataService.saveMonitoring(monitoring)
}
monitoring.active = false
dataService.saveMonitoring(monitoring)
}
}
@@ -245,7 +245,6 @@ class MonitoringService extends Logger {
)
}
private def lang(userId: Long) = localization.lang(userId)
@PostConstruct

View File

@@ -3,8 +3,8 @@ package com.lbs.server.actor
import akka.actor.ActorRef
import akka.testkit.TestProbe
import com.lbs.bot.model.{Command, Message, MessageSource, TelegramMessageSourceSystem}
import com.lbs.server.actor.Chat.Init
import com.lbs.server.actor.Login.{ForwardCommand, LoggedIn, UserId}
import com.lbs.server.actor.conversation.Conversation.{InitConversation, StartConversation}
import com.lbs.server.service.DataService
import org.mockito.Mockito._
@@ -42,7 +42,8 @@ class AuthSpec extends AkkaTestKit {
"initialize dialogue with login actor on /login command" in {
val cmd = Command(source, Message("1", Some("/login")))
auth ! cmd
loginActor.expectMsg(Init)
loginActor.expectMsg(InitConversation)
loginActor.expectMsg(StartConversation)
loginActor.expectMsg(cmd)
}
@@ -95,7 +96,8 @@ class AuthSpec extends AkkaTestKit {
"initialize dialogue with login actor on /login command" in {
val cmd = Command(source, Message("1", Some("/login")))
auth ! cmd
loginActor.expectMsg(Init)
loginActor.expectMsg(InitConversation)
loginActor.expectMsg(StartConversation)
loginActor.expectMsg(cmd)
}

View File

@@ -22,14 +22,14 @@ class ConversationSpec extends AkkaTestKit {
private var conf: String = _
def configure: EC =
def configure: Step =
externalConfig {
case Msg(confStr: String, data) =>
conf = confStr
goto(askHello) using data.copy(configured = true)
}
def askHello: QA =
def askHello: Step =
question { data =>
self ! Hello
} answer {
@@ -37,7 +37,7 @@ class ConversationSpec extends AkkaTestKit {
goto(askWorld) using data.copy(hello = "hello")
}
def askWorld: QA =
def askWorld: Step =
question { data =>
self ! World
} answer {
@@ -45,7 +45,7 @@ class ConversationSpec extends AkkaTestKit {
goto(askDialogue) using data.copy(world = "world")
}
def askDialogue: QA =
def askDialogue: Step =
question { data =>
self ! Dialogue
} answer {
@@ -80,17 +80,17 @@ class ConversationSpec extends AkkaTestKit {
class TestActor(originator: ActorRef) extends Conversation[Data] {
def configure1: IC =
def configure1: Step =
internalConfig { _ =>
goto(configure2) using Data(configured = true)
}
def configure2: IC =
def configure2: Step =
internalConfig { data =>
goto(askMessage2) using data.copy(message1 = "hello")
}
def askMessage2: QA =
def askMessage2: Step =
question { _ =>
self ! InvokeEnrichMessage
} answer {