From 207adfc96f0e2a6946d3cdbed828880e62635c9d Mon Sep 17 00:00:00 2001 From: Eugene Zadyra Date: Tue, 3 Jul 2018 22:24:27 +0200 Subject: [PATCH] Use "conversation fsm" for book and chat actors --- .../scala/com/lbs/server/actor/Account.scala | 6 +- .../scala/com/lbs/server/actor/Auth.scala | 5 +- .../scala/com/lbs/server/actor/Book.scala | 325 +++++++----------- .../main/scala/com/lbs/server/actor/Bug.scala | 4 +- .../scala/com/lbs/server/actor/Chat.scala | 122 +++---- .../com/lbs/server/actor/DatePicker.scala | 4 +- .../scala/com/lbs/server/actor/Help.scala | 2 +- .../scala/com/lbs/server/actor/History.scala | 4 +- .../scala/com/lbs/server/actor/Login.scala | 8 +- .../com/lbs/server/actor/Monitorings.scala | 6 +- .../scala/com/lbs/server/actor/Pager.scala | 4 +- .../scala/com/lbs/server/actor/Settings.scala | 4 +- .../com/lbs/server/actor/StaticData.scala | 6 +- .../server/actor/StaticDataForBooking.scala | 39 +-- .../scala/com/lbs/server/actor/Visits.scala | 6 +- .../server/actor/conversation/Domain.scala | 8 - .../scala/com/lbs/server/actor/package.scala | 7 - .../server/service/MonitoringService.scala | 5 +- .../scala/com/lbs/server/actor/AuthSpec.scala | 8 +- .../actor/conversation/ConversationSpec.scala | 14 +- 20 files changed, 233 insertions(+), 354 deletions(-) diff --git a/server/src/main/scala/com/lbs/server/actor/Account.scala b/server/src/main/scala/com/lbs/server/actor/Account.scala index 2dd11cf..24e81fa 100644 --- a/server/src/main/scala/com/lbs/server/actor/Account.scala +++ b/server/src/main/scala/com/lbs/server/actor/Account.scala @@ -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 { diff --git a/server/src/main/scala/com/lbs/server/actor/Auth.scala b/server/src/main/scala/com/lbs/server/actor/Auth.scala index 6d3dbce..5c357a9 100644 --- a/server/src/main/scala/com/lbs/server/actor/Auth.scala +++ b/server/src/main/scala/com/lbs/server/actor/Auth.scala @@ -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 diff --git a/server/src/main/scala/com/lbs/server/actor/Book.scala b/server/src/main/scala/com/lbs/server/actor/Book.scala index b15069d..b560384 100644 --- a/server/src/main/scala/com/lbs/server/actor/Book.scala +++ b/server/src/main/scala/com/lbs/server/actor/Book.scala @@ -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" diff --git a/server/src/main/scala/com/lbs/server/actor/Bug.scala b/server/src/main/scala/com/lbs/server/actor/Bug.scala index aef8e60..0a2400d 100644 --- a/server/src/main/scala/com/lbs/server/actor/Bug.scala +++ b/server/src/main/scala/com/lbs/server/actor/Bug.scala @@ -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 diff --git a/server/src/main/scala/com/lbs/server/actor/Chat.scala b/server/src/main/scala/com/lbs/server/actor/Chat.scala index 4ff3629..ba9e67e 100644 --- a/server/src/main/scala/com/lbs/server/actor/Chat.scala +++ b/server/src/main/scala/com/lbs/server/actor/Chat.scala @@ -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 } \ No newline at end of file diff --git a/server/src/main/scala/com/lbs/server/actor/DatePicker.scala b/server/src/main/scala/com/lbs/server/actor/DatePicker.scala index 4710cc6..3c35533 100644 --- a/server/src/main/scala/com/lbs/server/actor/DatePicker.scala +++ b/server/src/main/scala/com/lbs/server/actor/DatePicker.scala @@ -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 diff --git a/server/src/main/scala/com/lbs/server/actor/Help.scala b/server/src/main/scala/com/lbs/server/actor/Help.scala index 50ae3c0..0cd18a7 100644 --- a/server/src/main/scala/com/lbs/server/actor/Help.scala +++ b/server/src/main/scala/com/lbs/server/actor/Help.scala @@ -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) diff --git a/server/src/main/scala/com/lbs/server/actor/History.scala b/server/src/main/scala/com/lbs/server/actor/History.scala index 692d507..a09d525 100644 --- a/server/src/main/scala/com/lbs/server/actor/History.scala +++ b/server/src/main/scala/com/lbs/server/actor/History.scala @@ -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 diff --git a/server/src/main/scala/com/lbs/server/actor/Login.scala b/server/src/main/scala/com/lbs/server/actor/Login.scala index 6925474..4091ba8 100644 --- a/server/src/main/scala/com/lbs/server/actor/Login.scala +++ b/server/src/main/scala/com/lbs/server/actor/Login.scala @@ -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 { diff --git a/server/src/main/scala/com/lbs/server/actor/Monitorings.scala b/server/src/main/scala/com/lbs/server/actor/Monitorings.scala index ec8c265..20fea0e 100644 --- a/server/src/main/scala/com/lbs/server/actor/Monitorings.scala +++ b/server/src/main/scala/com/lbs/server/actor/Monitorings.scala @@ -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)))) diff --git a/server/src/main/scala/com/lbs/server/actor/Pager.scala b/server/src/main/scala/com/lbs/server/actor/Pager.scala index 5c9ef88..dfd4c34 100644 --- a/server/src/main/scala/com/lbs/server/actor/Pager.scala +++ b/server/src/main/scala/com/lbs/server/actor/Pager.scala @@ -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 { diff --git a/server/src/main/scala/com/lbs/server/actor/Settings.scala b/server/src/main/scala/com/lbs/server/actor/Settings.scala index 8c69c8f..a103048 100644 --- a/server/src/main/scala/com/lbs/server/actor/Settings.scala +++ b/server/src/main/scala/com/lbs/server/actor/Settings.scala @@ -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)) diff --git a/server/src/main/scala/com/lbs/server/actor/StaticData.scala b/server/src/main/scala/com/lbs/server/actor/StaticData.scala index dc817f4..3164a17 100644 --- a/server/src/main/scala/com/lbs/server/actor/StaticData.scala +++ b/server/src/main/scala/com/lbs/server/actor/StaticData.scala @@ -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)) diff --git a/server/src/main/scala/com/lbs/server/actor/StaticDataForBooking.scala b/server/src/main/scala/com/lbs/server/actor/StaticDataForBooking.scala index 64d97ae..3f25e4a 100644 --- a/server/src/main/scala/com/lbs/server/actor/StaticDataForBooking.scala +++ b/server/src/main/scala/com/lbs/server/actor/StaticDataForBooking.scala @@ -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) } } diff --git a/server/src/main/scala/com/lbs/server/actor/Visits.scala b/server/src/main/scala/com/lbs/server/actor/Visits.scala index f513d16..7a22b2c 100644 --- a/server/src/main/scala/com/lbs/server/actor/Visits.scala +++ b/server/src/main/scala/com/lbs/server/actor/Visits.scala @@ -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)))) diff --git a/server/src/main/scala/com/lbs/server/actor/conversation/Domain.scala b/server/src/main/scala/com/lbs/server/actor/conversation/Domain.scala index 3b9f5bf..454a970 100644 --- a/server/src/main/scala/com/lbs/server/actor/conversation/Domain.scala +++ b/server/src/main/scala/com/lbs/server/actor/conversation/Domain.scala @@ -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)) } diff --git a/server/src/main/scala/com/lbs/server/actor/package.scala b/server/src/main/scala/com/lbs/server/actor/package.scala index 1f8e90c..551c8f1 100644 --- a/server/src/main/scala/com/lbs/server/actor/package.scala +++ b/server/src/main/scala/com/lbs/server/actor/package.scala @@ -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 - } diff --git a/server/src/main/scala/com/lbs/server/service/MonitoringService.scala b/server/src/main/scala/com/lbs/server/service/MonitoringService.scala index e6b31b3..7ba0cbe 100644 --- a/server/src/main/scala/com/lbs/server/service/MonitoringService.scala +++ b/server/src/main/scala/com/lbs/server/service/MonitoringService.scala @@ -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 diff --git a/server/src/test/scala/com/lbs/server/actor/AuthSpec.scala b/server/src/test/scala/com/lbs/server/actor/AuthSpec.scala index acd754b..34f174d 100644 --- a/server/src/test/scala/com/lbs/server/actor/AuthSpec.scala +++ b/server/src/test/scala/com/lbs/server/actor/AuthSpec.scala @@ -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) } diff --git a/server/src/test/scala/com/lbs/server/actor/conversation/ConversationSpec.scala b/server/src/test/scala/com/lbs/server/actor/conversation/ConversationSpec.scala index 59d8d8f..9f11e53 100644 --- a/server/src/test/scala/com/lbs/server/actor/conversation/ConversationSpec.scala +++ b/server/src/test/scala/com/lbs/server/actor/conversation/ConversationSpec.scala @@ -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 {